@graffiti-garden/implementation-decentralized 0.0.1

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.
Files changed (193) hide show
  1. package/LICENSE +674 -0
  2. package/dist/1-services/1-authorization.d.ts +37 -0
  3. package/dist/1-services/1-authorization.d.ts.map +1 -0
  4. package/dist/1-services/2-dids-tests.d.ts +2 -0
  5. package/dist/1-services/2-dids-tests.d.ts.map +1 -0
  6. package/dist/1-services/2-dids.d.ts +9 -0
  7. package/dist/1-services/2-dids.d.ts.map +1 -0
  8. package/dist/1-services/3-storage-buckets-tests.d.ts +2 -0
  9. package/dist/1-services/3-storage-buckets-tests.d.ts.map +1 -0
  10. package/dist/1-services/3-storage-buckets.d.ts +11 -0
  11. package/dist/1-services/3-storage-buckets.d.ts.map +1 -0
  12. package/dist/1-services/4-inboxes-tests.d.ts +2 -0
  13. package/dist/1-services/4-inboxes-tests.d.ts.map +1 -0
  14. package/dist/1-services/4-inboxes.d.ts +87 -0
  15. package/dist/1-services/4-inboxes.d.ts.map +1 -0
  16. package/dist/1-services/utilities.d.ts +7 -0
  17. package/dist/1-services/utilities.d.ts.map +1 -0
  18. package/dist/2-primitives/1-string-encoding-tests.d.ts +2 -0
  19. package/dist/2-primitives/1-string-encoding-tests.d.ts.map +1 -0
  20. package/dist/2-primitives/1-string-encoding.d.ts +6 -0
  21. package/dist/2-primitives/1-string-encoding.d.ts.map +1 -0
  22. package/dist/2-primitives/2-content-addresses-tests.d.ts +2 -0
  23. package/dist/2-primitives/2-content-addresses-tests.d.ts.map +1 -0
  24. package/dist/2-primitives/2-content-addresses.d.ts +8 -0
  25. package/dist/2-primitives/2-content-addresses.d.ts.map +1 -0
  26. package/dist/2-primitives/3-channel-attestations-tests.d.ts +2 -0
  27. package/dist/2-primitives/3-channel-attestations-tests.d.ts.map +1 -0
  28. package/dist/2-primitives/3-channel-attestations.d.ts +13 -0
  29. package/dist/2-primitives/3-channel-attestations.d.ts.map +1 -0
  30. package/dist/2-primitives/4-allowed-attestations-tests.d.ts +2 -0
  31. package/dist/2-primitives/4-allowed-attestations-tests.d.ts.map +1 -0
  32. package/dist/2-primitives/4-allowed-attestations.d.ts +9 -0
  33. package/dist/2-primitives/4-allowed-attestations.d.ts.map +1 -0
  34. package/dist/3-protocol/1-sessions.d.ts +81 -0
  35. package/dist/3-protocol/1-sessions.d.ts.map +1 -0
  36. package/dist/3-protocol/2-handles-tests.d.ts +2 -0
  37. package/dist/3-protocol/2-handles-tests.d.ts.map +1 -0
  38. package/dist/3-protocol/2-handles.d.ts +13 -0
  39. package/dist/3-protocol/2-handles.d.ts.map +1 -0
  40. package/dist/3-protocol/3-object-encoding-tests.d.ts +2 -0
  41. package/dist/3-protocol/3-object-encoding-tests.d.ts.map +1 -0
  42. package/dist/3-protocol/3-object-encoding.d.ts +43 -0
  43. package/dist/3-protocol/3-object-encoding.d.ts.map +1 -0
  44. package/dist/3-protocol/4-graffiti.d.ts +79 -0
  45. package/dist/3-protocol/4-graffiti.d.ts.map +1 -0
  46. package/dist/3-protocol/login-dialog.html.d.ts +2 -0
  47. package/dist/3-protocol/login-dialog.html.d.ts.map +1 -0
  48. package/dist/browser/ajv-QBSREQSI.js +9 -0
  49. package/dist/browser/ajv-QBSREQSI.js.map +7 -0
  50. package/dist/browser/build-BXWPS7VK.js +2 -0
  51. package/dist/browser/build-BXWPS7VK.js.map +7 -0
  52. package/dist/browser/chunk-RFBBAUMM.js +2 -0
  53. package/dist/browser/chunk-RFBBAUMM.js.map +7 -0
  54. package/dist/browser/graffiti-KV3G3O72-URO7SJIJ.js +2 -0
  55. package/dist/browser/graffiti-KV3G3O72-URO7SJIJ.js.map +7 -0
  56. package/dist/browser/index.js +16 -0
  57. package/dist/browser/index.js.map +7 -0
  58. package/dist/browser/login-dialog.html-XUWYDNNI.js +44 -0
  59. package/dist/browser/login-dialog.html-XUWYDNNI.js.map +7 -0
  60. package/dist/browser/rock-salt-LI7DAH66-KPFEBIBO.js +2 -0
  61. package/dist/browser/rock-salt-LI7DAH66-KPFEBIBO.js.map +7 -0
  62. package/dist/browser/style-YUTCEBZV-RWYJV575.js +287 -0
  63. package/dist/browser/style-YUTCEBZV-RWYJV575.js.map +7 -0
  64. package/dist/cjs/1-services/1-authorization.js +317 -0
  65. package/dist/cjs/1-services/1-authorization.js.map +7 -0
  66. package/dist/cjs/1-services/2-dids-tests.js +44 -0
  67. package/dist/cjs/1-services/2-dids-tests.js.map +7 -0
  68. package/dist/cjs/1-services/2-dids.js +47 -0
  69. package/dist/cjs/1-services/2-dids.js.map +7 -0
  70. package/dist/cjs/1-services/3-storage-buckets-tests.js +123 -0
  71. package/dist/cjs/1-services/3-storage-buckets-tests.js.map +7 -0
  72. package/dist/cjs/1-services/3-storage-buckets.js +148 -0
  73. package/dist/cjs/1-services/3-storage-buckets.js.map +7 -0
  74. package/dist/cjs/1-services/4-inboxes-tests.js +145 -0
  75. package/dist/cjs/1-services/4-inboxes-tests.js.map +7 -0
  76. package/dist/cjs/1-services/4-inboxes.js +539 -0
  77. package/dist/cjs/1-services/4-inboxes.js.map +7 -0
  78. package/dist/cjs/1-services/utilities.js +75 -0
  79. package/dist/cjs/1-services/utilities.js.map +7 -0
  80. package/dist/cjs/2-primitives/1-string-encoding-tests.js +50 -0
  81. package/dist/cjs/2-primitives/1-string-encoding-tests.js.map +7 -0
  82. package/dist/cjs/2-primitives/1-string-encoding.js +46 -0
  83. package/dist/cjs/2-primitives/1-string-encoding.js.map +7 -0
  84. package/dist/cjs/2-primitives/2-content-addresses-tests.js +62 -0
  85. package/dist/cjs/2-primitives/2-content-addresses-tests.js.map +7 -0
  86. package/dist/cjs/2-primitives/2-content-addresses.js +53 -0
  87. package/dist/cjs/2-primitives/2-content-addresses.js.map +7 -0
  88. package/dist/cjs/2-primitives/3-channel-attestations-tests.js +130 -0
  89. package/dist/cjs/2-primitives/3-channel-attestations-tests.js.map +7 -0
  90. package/dist/cjs/2-primitives/3-channel-attestations.js +84 -0
  91. package/dist/cjs/2-primitives/3-channel-attestations.js.map +7 -0
  92. package/dist/cjs/2-primitives/4-allowed-attestations-tests.js +96 -0
  93. package/dist/cjs/2-primitives/4-allowed-attestations-tests.js.map +7 -0
  94. package/dist/cjs/2-primitives/4-allowed-attestations.js +68 -0
  95. package/dist/cjs/2-primitives/4-allowed-attestations.js.map +7 -0
  96. package/dist/cjs/3-protocol/1-sessions.js +473 -0
  97. package/dist/cjs/3-protocol/1-sessions.js.map +7 -0
  98. package/dist/cjs/3-protocol/2-handles-tests.js +39 -0
  99. package/dist/cjs/3-protocol/2-handles-tests.js.map +7 -0
  100. package/dist/cjs/3-protocol/2-handles.js +65 -0
  101. package/dist/cjs/3-protocol/2-handles.js.map +7 -0
  102. package/dist/cjs/3-protocol/3-object-encoding-tests.js +253 -0
  103. package/dist/cjs/3-protocol/3-object-encoding-tests.js.map +7 -0
  104. package/dist/cjs/3-protocol/3-object-encoding.js +287 -0
  105. package/dist/cjs/3-protocol/3-object-encoding.js.map +7 -0
  106. package/dist/cjs/3-protocol/4-graffiti.js +937 -0
  107. package/dist/cjs/3-protocol/4-graffiti.js.map +7 -0
  108. package/dist/cjs/3-protocol/login-dialog.html.js +67 -0
  109. package/dist/cjs/3-protocol/login-dialog.html.js.map +7 -0
  110. package/dist/cjs/index.js +32 -0
  111. package/dist/cjs/index.js.map +7 -0
  112. package/dist/cjs/index.spec.js +130 -0
  113. package/dist/cjs/index.spec.js.map +7 -0
  114. package/dist/esm/1-services/1-authorization.js +304 -0
  115. package/dist/esm/1-services/1-authorization.js.map +7 -0
  116. package/dist/esm/1-services/2-dids-tests.js +24 -0
  117. package/dist/esm/1-services/2-dids-tests.js.map +7 -0
  118. package/dist/esm/1-services/2-dids.js +27 -0
  119. package/dist/esm/1-services/2-dids.js.map +7 -0
  120. package/dist/esm/1-services/3-storage-buckets-tests.js +103 -0
  121. package/dist/esm/1-services/3-storage-buckets-tests.js.map +7 -0
  122. package/dist/esm/1-services/3-storage-buckets.js +132 -0
  123. package/dist/esm/1-services/3-storage-buckets.js.map +7 -0
  124. package/dist/esm/1-services/4-inboxes-tests.js +125 -0
  125. package/dist/esm/1-services/4-inboxes-tests.js.map +7 -0
  126. package/dist/esm/1-services/4-inboxes.js +533 -0
  127. package/dist/esm/1-services/4-inboxes.js.map +7 -0
  128. package/dist/esm/1-services/utilities.js +60 -0
  129. package/dist/esm/1-services/utilities.js.map +7 -0
  130. package/dist/esm/2-primitives/1-string-encoding-tests.js +33 -0
  131. package/dist/esm/2-primitives/1-string-encoding-tests.js.map +7 -0
  132. package/dist/esm/2-primitives/1-string-encoding.js +26 -0
  133. package/dist/esm/2-primitives/1-string-encoding.js.map +7 -0
  134. package/dist/esm/2-primitives/2-content-addresses-tests.js +45 -0
  135. package/dist/esm/2-primitives/2-content-addresses-tests.js.map +7 -0
  136. package/dist/esm/2-primitives/2-content-addresses.js +33 -0
  137. package/dist/esm/2-primitives/2-content-addresses.js.map +7 -0
  138. package/dist/esm/2-primitives/3-channel-attestations-tests.js +116 -0
  139. package/dist/esm/2-primitives/3-channel-attestations-tests.js.map +7 -0
  140. package/dist/esm/2-primitives/3-channel-attestations.js +69 -0
  141. package/dist/esm/2-primitives/3-channel-attestations.js.map +7 -0
  142. package/dist/esm/2-primitives/4-allowed-attestations-tests.js +82 -0
  143. package/dist/esm/2-primitives/4-allowed-attestations-tests.js.map +7 -0
  144. package/dist/esm/2-primitives/4-allowed-attestations.js +51 -0
  145. package/dist/esm/2-primitives/4-allowed-attestations.js.map +7 -0
  146. package/dist/esm/3-protocol/1-sessions.js +465 -0
  147. package/dist/esm/3-protocol/1-sessions.js.map +7 -0
  148. package/dist/esm/3-protocol/2-handles-tests.js +19 -0
  149. package/dist/esm/3-protocol/2-handles-tests.js.map +7 -0
  150. package/dist/esm/3-protocol/2-handles.js +45 -0
  151. package/dist/esm/3-protocol/2-handles.js.map +7 -0
  152. package/dist/esm/3-protocol/3-object-encoding-tests.js +248 -0
  153. package/dist/esm/3-protocol/3-object-encoding-tests.js.map +7 -0
  154. package/dist/esm/3-protocol/3-object-encoding.js +280 -0
  155. package/dist/esm/3-protocol/3-object-encoding.js.map +7 -0
  156. package/dist/esm/3-protocol/4-graffiti.js +957 -0
  157. package/dist/esm/3-protocol/4-graffiti.js.map +7 -0
  158. package/dist/esm/3-protocol/login-dialog.html.js +47 -0
  159. package/dist/esm/3-protocol/login-dialog.html.js.map +7 -0
  160. package/dist/esm/index.js +14 -0
  161. package/dist/esm/index.js.map +7 -0
  162. package/dist/esm/index.spec.js +133 -0
  163. package/dist/esm/index.spec.js.map +7 -0
  164. package/dist/index.d.ts +10 -0
  165. package/dist/index.d.ts.map +1 -0
  166. package/dist/index.spec.d.ts +2 -0
  167. package/dist/index.spec.d.ts.map +1 -0
  168. package/package.json +62 -0
  169. package/src/1-services/1-authorization.ts +399 -0
  170. package/src/1-services/2-dids-tests.ts +24 -0
  171. package/src/1-services/2-dids.ts +30 -0
  172. package/src/1-services/3-storage-buckets-tests.ts +121 -0
  173. package/src/1-services/3-storage-buckets.ts +183 -0
  174. package/src/1-services/4-inboxes-tests.ts +154 -0
  175. package/src/1-services/4-inboxes.ts +722 -0
  176. package/src/1-services/utilities.ts +65 -0
  177. package/src/2-primitives/1-string-encoding-tests.ts +33 -0
  178. package/src/2-primitives/1-string-encoding.ts +33 -0
  179. package/src/2-primitives/2-content-addresses-tests.ts +46 -0
  180. package/src/2-primitives/2-content-addresses.ts +42 -0
  181. package/src/2-primitives/3-channel-attestations-tests.ts +125 -0
  182. package/src/2-primitives/3-channel-attestations.ts +95 -0
  183. package/src/2-primitives/4-allowed-attestations-tests.ts +86 -0
  184. package/src/2-primitives/4-allowed-attestations.ts +69 -0
  185. package/src/3-protocol/1-sessions.ts +601 -0
  186. package/src/3-protocol/2-handles-tests.ts +17 -0
  187. package/src/3-protocol/2-handles.ts +60 -0
  188. package/src/3-protocol/3-object-encoding-tests.ts +269 -0
  189. package/src/3-protocol/3-object-encoding.ts +396 -0
  190. package/src/3-protocol/4-graffiti.ts +1265 -0
  191. package/src/3-protocol/login-dialog.html.ts +43 -0
  192. package/src/index.spec.ts +158 -0
  193. package/src/index.ts +16 -0
@@ -0,0 +1,65 @@
1
+ import {
2
+ GraffitiErrorCursorExpired,
3
+ GraffitiErrorForbidden,
4
+ GraffitiErrorNotFound,
5
+ GraffitiErrorTooLarge,
6
+ } from "@graffiti-garden/api";
7
+
8
+ const SERVICE_ENDPOINT_PREFIX_HTTPS = "https://";
9
+ export function verifyHTTPSEndpoint(endpoint: string): void {
10
+ if (!endpoint.startsWith(SERVICE_ENDPOINT_PREFIX_HTTPS)) {
11
+ throw new Error("Unrecognized storage bucket endpoint type");
12
+ }
13
+ }
14
+
15
+ export async function getAuthorizationEndpoint(
16
+ serviceEndpoint: string,
17
+ ): Promise<string> {
18
+ verifyHTTPSEndpoint(serviceEndpoint);
19
+ const authUrl = `${serviceEndpoint}/auth`;
20
+
21
+ const response = await fetch(authUrl);
22
+ if (!response.ok) {
23
+ throw new Error("Failed to get storage bucket authorization endpoint");
24
+ }
25
+ return await response.text();
26
+ }
27
+
28
+ export class GraffitiErrorUnauthorized extends Error {
29
+ constructor(message?: string) {
30
+ super(message);
31
+ this.name = "GraffitiErrorUnauthorized";
32
+ Object.setPrototypeOf(this, GraffitiErrorUnauthorized.prototype);
33
+ }
34
+ }
35
+
36
+ export async function fetchWithErrorHandling(
37
+ ...args: Parameters<typeof fetch>
38
+ ) {
39
+ const response = await fetch(...args);
40
+
41
+ if (!response.ok) {
42
+ let errorText: string;
43
+ try {
44
+ errorText = await response.text();
45
+ } catch {
46
+ errorText = response.statusText;
47
+ }
48
+
49
+ if (response.status === 401) {
50
+ throw new GraffitiErrorUnauthorized(errorText);
51
+ } else if (response.status === 403) {
52
+ throw new GraffitiErrorForbidden(errorText);
53
+ } else if (response.status === 404) {
54
+ throw new GraffitiErrorNotFound(errorText);
55
+ } else if (response.status === 410) {
56
+ throw new GraffitiErrorCursorExpired(errorText);
57
+ } else if (response.status === 413) {
58
+ throw new GraffitiErrorTooLarge(errorText);
59
+ } else {
60
+ throw new Error(errorText);
61
+ }
62
+ }
63
+
64
+ return response;
65
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ STRING_ENCODER_METHOD_BASE64URL,
4
+ StringEncoder,
5
+ } from "./1-string-encoding";
6
+ import { randomBytes } from "@noble/hashes/utils.js";
7
+
8
+ export function stringEncodingTests() {
9
+ describe("String encoding tests", () => {
10
+ const stringEncodingMethods = [STRING_ENCODER_METHOD_BASE64URL];
11
+ const stringEncoder = new StringEncoder();
12
+
13
+ test("Invalid string decoding method", async () => {
14
+ const bytes = randomBytes();
15
+ await expect(() =>
16
+ stringEncoder.encode("invalid-method", bytes),
17
+ ).rejects.toThrow();
18
+ });
19
+
20
+ for (const method of stringEncodingMethods) {
21
+ describe(`String Encoding Method: ${method}`, () => {
22
+ test("encodes and decodes strings correctly", async () => {
23
+ const bytes = randomBytes();
24
+ const encoded = await stringEncoder.encode(method, bytes);
25
+ const decoded = await stringEncoder.decode(encoded);
26
+
27
+ expect(decoded).toEqual(bytes);
28
+ expect(decoded).not.toEqual(randomBytes());
29
+ });
30
+ });
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,33 @@
1
+ // https://github.com/multiformats/multibase/blob/master/multibase.csv
2
+ export const STRING_ENCODER_METHOD_BASE64URL = "base64url";
3
+ const STRING_ENCODER_PREFIX_BASE64URL = "u";
4
+
5
+ export class StringEncoder {
6
+ async encode(method: string, bytes: Uint8Array): Promise<string> {
7
+ if (method !== STRING_ENCODER_METHOD_BASE64URL) {
8
+ throw new Error(`Unsupported string encoding method: ${method}`);
9
+ }
10
+ // Convert it to base64
11
+ const base64 = btoa(String.fromCodePoint(...bytes));
12
+ // Make sure it is url safe
13
+ const encoded = base64
14
+ .replace(/\+/g, "-")
15
+ .replace(/\//g, "_")
16
+ .replace(/\=+$/, "");
17
+ // Append method prefix
18
+ return STRING_ENCODER_PREFIX_BASE64URL + encoded;
19
+ }
20
+
21
+ async decode(base64Url: string): Promise<Uint8Array> {
22
+ if (!base64Url.startsWith(STRING_ENCODER_PREFIX_BASE64URL)) {
23
+ throw new Error(`Unsupported string encoding prefix: ${base64Url[0]}`);
24
+ }
25
+ base64Url = base64Url.slice(1);
26
+ // Undo url-safe base64
27
+ let base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
28
+ // Add padding if necessary
29
+ while (base64.length % 4 !== 0) base64 += "=";
30
+ // Decode
31
+ return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
32
+ }
33
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ CONTENT_ADDRESS_METHOD_SHA256,
4
+ ContentAddresses,
5
+ } from "./2-content-addresses";
6
+ import { randomBytes } from "@noble/hashes/utils.js";
7
+
8
+ export function contentAddressesTests() {
9
+ describe("Content Address Tests", () => {
10
+ const contentAddressMethods = [CONTENT_ADDRESS_METHOD_SHA256];
11
+ const contentAddresses = new ContentAddresses();
12
+
13
+ test("Invalid content address method", async () => {
14
+ const bytes = randomBytes();
15
+ await expect(() =>
16
+ contentAddresses.register("invalid-method", bytes),
17
+ ).rejects.toThrow();
18
+ });
19
+
20
+ for (const method of contentAddressMethods) {
21
+ describe(`Content Address Method: ${method}`, () => {
22
+ test("idempotent addresses", async () => {
23
+ const bytes = randomBytes();
24
+ const address1 = await contentAddresses.register(method, bytes);
25
+ const address2 = await contentAddresses.register(method, bytes);
26
+ expect(address1).toEqual(address2);
27
+ });
28
+
29
+ test("unique adddresses", async () => {
30
+ const bytes1 = randomBytes();
31
+ const bytes2 = randomBytes();
32
+ const address1 = await contentAddresses.register(method, bytes1);
33
+ const address2 = await contentAddresses.register(method, bytes2);
34
+ expect(address1).not.toEqual(address2);
35
+ });
36
+
37
+ test("get method", async () => {
38
+ const bytes = randomBytes();
39
+ const address = await contentAddresses.register(method, bytes);
40
+ const retrievedMethod = await contentAddresses.getMethod(address);
41
+ expect(retrievedMethod).toEqual(method);
42
+ });
43
+ });
44
+ }
45
+ });
46
+ }
@@ -0,0 +1,42 @@
1
+ import { sha256 } from "@noble/hashes/webcrypto.js";
2
+
3
+ export const CONTENT_ADDRESS_METHOD_SHA256 = "sha2-256";
4
+
5
+ // Multihash code and length for SHA2-256
6
+ // https://multiformats.io/multihash/#sha2-256---256-bits-aka-sha256
7
+ export const MULTIHASH_CODE_SHA256 = 0x12;
8
+ export const MULTIHASH_LENGTH_SHA256 = 32;
9
+
10
+ export class ContentAddresses {
11
+ async register(
12
+ contentAddressMethod: string,
13
+ data: Uint8Array,
14
+ ): Promise<Uint8Array> {
15
+ if (contentAddressMethod !== CONTENT_ADDRESS_METHOD_SHA256) {
16
+ throw new Error(
17
+ `Unsupported content address method: ${contentAddressMethod}`,
18
+ );
19
+ }
20
+
21
+ const hash = await sha256(data);
22
+
23
+ const prefixedHash = new Uint8Array(2 + hash.length);
24
+ prefixedHash[0] = MULTIHASH_CODE_SHA256;
25
+ prefixedHash[1] = MULTIHASH_LENGTH_SHA256;
26
+ prefixedHash.set(hash, 2);
27
+
28
+ return prefixedHash;
29
+ }
30
+
31
+ async getMethod(contentAddress: Uint8Array): Promise<string> {
32
+ if (
33
+ contentAddress[0] === MULTIHASH_CODE_SHA256 &&
34
+ contentAddress[1] === MULTIHASH_LENGTH_SHA256 &&
35
+ contentAddress.length === 2 + MULTIHASH_LENGTH_SHA256
36
+ ) {
37
+ return CONTENT_ADDRESS_METHOD_SHA256;
38
+ } else {
39
+ throw new Error(`Unrecognized content address format.`);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,125 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ CHANNEL_ATTESTATION_METHOD_SHA256_ED25519,
4
+ ChannelAttestations,
5
+ } from "./3-channel-attestations";
6
+ import { randomBytes } from "@noble/hashes/utils.js";
7
+ import {
8
+ StringEncoder,
9
+ STRING_ENCODER_METHOD_BASE64URL,
10
+ } from "./1-string-encoding";
11
+
12
+ export function channelAttestationTests() {
13
+ describe("Channel Attestation Tests", () => {
14
+ const allowedAttestationMethods = [
15
+ CHANNEL_ATTESTATION_METHOD_SHA256_ED25519,
16
+ ];
17
+ const channelAttestations = new ChannelAttestations();
18
+
19
+ async function randomActor() {
20
+ const bytes = randomBytes();
21
+ const str = await new StringEncoder().encode(
22
+ STRING_ENCODER_METHOD_BASE64URL,
23
+ bytes,
24
+ );
25
+ return `did:web:${str}.com`;
26
+ }
27
+ async function randomChannel() {
28
+ const bytes = randomBytes();
29
+ return await new StringEncoder().encode(
30
+ STRING_ENCODER_METHOD_BASE64URL,
31
+ bytes,
32
+ );
33
+ }
34
+
35
+ test("Invalid attestation method", async () => {
36
+ const actor = await randomActor();
37
+ await expect(() =>
38
+ channelAttestations.register("invalid-method", actor),
39
+ ).rejects.toThrow();
40
+ });
41
+
42
+ for (const method of allowedAttestationMethods) {
43
+ describe(`Attestation Method: ${method}`, () => {
44
+ test("get method", async () => {
45
+ const channel = await randomChannel();
46
+ const publicId = await channelAttestations.register(method, channel);
47
+ const methodReturned = await channelAttestations.getMethod(publicId);
48
+ expect(methodReturned).toEqual(method);
49
+ });
50
+
51
+ test("Idempotent public Ids", async () => {
52
+ const channel = await randomChannel();
53
+ const publicId1 = await channelAttestations.register(method, channel);
54
+ const publicId2 = await channelAttestations.register(method, channel);
55
+ expect(publicId1).toEqual(publicId2);
56
+ });
57
+
58
+ test("Unique public ids", async () => {
59
+ const channel1 = await randomChannel();
60
+ const channel2 = await randomChannel();
61
+ const publicId1 = await channelAttestations.register(
62
+ method,
63
+ channel1,
64
+ );
65
+ const publicId2 = await channelAttestations.register(
66
+ method,
67
+ channel2,
68
+ );
69
+ expect(publicId1).not.toEqual(publicId2);
70
+ });
71
+
72
+ test("Valid attestation", async () => {
73
+ const actor = await randomActor();
74
+ const channel = await randomChannel();
75
+ const { attestation, channelPublicId } =
76
+ await channelAttestations.attest(method, actor, channel);
77
+ const isValid = await channelAttestations.validate(
78
+ attestation,
79
+ actor,
80
+ channelPublicId,
81
+ );
82
+ expect(isValid).toBe(true);
83
+
84
+ const channelPublicIdSeperate = await channelAttestations.register(
85
+ method,
86
+ channel,
87
+ );
88
+ expect(channelPublicId).toEqual(channelPublicIdSeperate);
89
+ });
90
+
91
+ test("Invalid attestation with wrong actor", async () => {
92
+ const actor = await randomActor();
93
+ const wrongActor = await randomActor();
94
+ const channel = await randomChannel();
95
+ const { attestation, channelPublicId } =
96
+ await channelAttestations.attest(method, actor, channel);
97
+ const isValid = await channelAttestations.validate(
98
+ attestation,
99
+ wrongActor,
100
+ channelPublicId,
101
+ );
102
+ expect(isValid).toBe(false);
103
+ });
104
+
105
+ test("Invalid attestation with wrong channel", async () => {
106
+ const actor = await randomActor();
107
+ const channel = await randomChannel();
108
+ const wrongChannel = await randomChannel();
109
+ const { attestation, channelPublicId } =
110
+ await channelAttestations.attest(method, actor, channel);
111
+ const wrongChannelPublicId = await channelAttestations.register(
112
+ method,
113
+ wrongChannel,
114
+ );
115
+ const isValid = await channelAttestations.validate(
116
+ attestation,
117
+ actor,
118
+ wrongChannelPublicId,
119
+ );
120
+ expect(isValid).toBe(false);
121
+ });
122
+ });
123
+ }
124
+ });
125
+ }
@@ -0,0 +1,95 @@
1
+ import {
2
+ getPublicKeyAsync,
3
+ signAsync,
4
+ verifyAsync,
5
+ hashes,
6
+ } from "@noble/ed25519";
7
+ import { sha256, sha512 } from "@noble/hashes/webcrypto.js";
8
+ hashes.sha512Async = sha512;
9
+
10
+ export const CHANNEL_ATTESTATION_METHOD_SHA256_ED25519 = "pk:sha2-256+ed25519";
11
+ const CHANNEL_ATTESTATION_METHOD_PREFIX_SHA256_ED25519 = 0;
12
+
13
+ export class ChannelAttestations {
14
+ async register(
15
+ channelAttestationMethod: string,
16
+ channel: string,
17
+ ): Promise<Uint8Array> {
18
+ if (
19
+ channelAttestationMethod !== CHANNEL_ATTESTATION_METHOD_SHA256_ED25519
20
+ ) {
21
+ throw new Error(
22
+ `Unsupported channel attestation method: ${channelAttestationMethod}`,
23
+ );
24
+ }
25
+ const privateKey = await this.channelToPrivateKey(channel);
26
+ return await this.channelPublicIdFromPrivateKey(privateKey);
27
+ }
28
+
29
+ async getMethod(channelPublicId: Uint8Array): Promise<string> {
30
+ if (
31
+ channelPublicId[0] === CHANNEL_ATTESTATION_METHOD_PREFIX_SHA256_ED25519
32
+ ) {
33
+ return CHANNEL_ATTESTATION_METHOD_SHA256_ED25519;
34
+ } else {
35
+ throw new Error(`Unrecognized channel attestation method.`);
36
+ }
37
+ }
38
+
39
+ protected async channelToPrivateKey(channel: string): Promise<Uint8Array> {
40
+ const channelBytes = new TextEncoder().encode(channel);
41
+ return await sha256(channelBytes);
42
+ }
43
+ protected async channelPublicIdFromPrivateKey(
44
+ privateKey: Uint8Array,
45
+ ): Promise<Uint8Array> {
46
+ const channelPublicIdRaw = await getPublicKeyAsync(privateKey);
47
+ const channelPublicId = new Uint8Array(channelPublicIdRaw.length + 1);
48
+ channelPublicId[0] = CHANNEL_ATTESTATION_METHOD_PREFIX_SHA256_ED25519;
49
+ channelPublicId.set(channelPublicIdRaw, 1);
50
+ return channelPublicId;
51
+ }
52
+
53
+ async attest(
54
+ channelAttestationMethod: string,
55
+ actor: string,
56
+ channel: string,
57
+ ): Promise<{
58
+ attestation: Uint8Array;
59
+ channelPublicId: Uint8Array;
60
+ }> {
61
+ if (
62
+ channelAttestationMethod !== CHANNEL_ATTESTATION_METHOD_SHA256_ED25519
63
+ ) {
64
+ throw new Error(
65
+ `Unsupported channel attestation method: ${channelAttestationMethod}`,
66
+ );
67
+ }
68
+ const privateKey = await this.channelToPrivateKey(channel);
69
+ const channelPublicId =
70
+ await this.channelPublicIdFromPrivateKey(privateKey);
71
+
72
+ const actorBytes = new TextEncoder().encode(actor);
73
+ const attestation = await signAsync(actorBytes, privateKey);
74
+ return { attestation, channelPublicId };
75
+ }
76
+
77
+ async validate(
78
+ attestation: Uint8Array,
79
+ actor: string,
80
+ channelPublicId: Uint8Array,
81
+ ): Promise<boolean> {
82
+ const prefix = channelPublicId[0];
83
+ if (prefix !== CHANNEL_ATTESTATION_METHOD_PREFIX_SHA256_ED25519) {
84
+ throw new Error(
85
+ `Unrecognized channel attestation method prefix: ${prefix}`,
86
+ );
87
+ }
88
+
89
+ return await verifyAsync(
90
+ attestation,
91
+ new TextEncoder().encode(actor),
92
+ channelPublicId.slice(1),
93
+ );
94
+ }
95
+ }
@@ -0,0 +1,86 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import {
3
+ ALLOWED_ATTESTATION_METHOD_HMAC_SHA256,
4
+ AllowedAttestations,
5
+ } from "./4-allowed-attestations";
6
+ import { randomBytes } from "@noble/hashes/utils.js";
7
+ import {
8
+ StringEncoder,
9
+ STRING_ENCODER_METHOD_BASE64URL,
10
+ } from "./1-string-encoding";
11
+
12
+ export function allowedAttestationTests() {
13
+ describe("Allowed Attestation Tests", () => {
14
+ const allowedAttestationMethods = [ALLOWED_ATTESTATION_METHOD_HMAC_SHA256];
15
+ const allowedAttestations = new AllowedAttestations();
16
+
17
+ async function randomActor() {
18
+ const bytes = randomBytes();
19
+ const str = await new StringEncoder().encode(
20
+ STRING_ENCODER_METHOD_BASE64URL,
21
+ bytes,
22
+ );
23
+ return `did:web:${str}.com`;
24
+ }
25
+
26
+ test("Invalid attestation method", async () => {
27
+ const actor = await randomActor();
28
+ await expect(() =>
29
+ allowedAttestations.attest("invalid-method", actor),
30
+ ).rejects.toThrow();
31
+ });
32
+
33
+ for (const method of allowedAttestationMethods) {
34
+ describe(`Attestation Method: ${method}`, () => {
35
+ test("Valid attestation", async () => {
36
+ const actor = await randomActor();
37
+ const { attestation, ticket } = await allowedAttestations.attest(
38
+ method,
39
+ actor,
40
+ );
41
+ const isValid = await allowedAttestations.validate(
42
+ attestation,
43
+ actor,
44
+ ticket,
45
+ );
46
+ expect(isValid).toBe(true);
47
+ });
48
+
49
+ test("Wrong actor", async () => {
50
+ const actor = await randomActor();
51
+ const { attestation, ticket } = await allowedAttestations.attest(
52
+ method,
53
+ actor,
54
+ );
55
+ const otherActor = await randomActor();
56
+ const isValid = await allowedAttestations.validate(
57
+ attestation,
58
+ otherActor,
59
+ ticket,
60
+ );
61
+ expect(isValid).toBe(false);
62
+ });
63
+
64
+ test("Wrong ticket", async () => {
65
+ const actor = await randomActor();
66
+ const { attestation: attestation1, ticket: ticket1 } =
67
+ await allowedAttestations.attest(method, actor);
68
+ const { attestation: attestation2, ticket: ticket2 } =
69
+ await allowedAttestations.attest(method, actor);
70
+ const isValid1 = await allowedAttestations.validate(
71
+ attestation1,
72
+ actor,
73
+ ticket2,
74
+ );
75
+ const isValid2 = await allowedAttestations.validate(
76
+ attestation2,
77
+ actor,
78
+ ticket1,
79
+ );
80
+ expect(isValid1).toBe(false);
81
+ expect(isValid2).toBe(false);
82
+ });
83
+ });
84
+ }
85
+ });
86
+ }
@@ -0,0 +1,69 @@
1
+ import { sha256, hmac } from "@noble/hashes/webcrypto.js";
2
+ import { randomBytes } from "@noble/hashes/utils.js";
3
+ import {
4
+ MULTIHASH_CODE_SHA256,
5
+ MULTIHASH_LENGTH_SHA256,
6
+ } from "./2-content-addresses";
7
+
8
+ export const ALLOWED_ATTESTATION_METHOD_HMAC_SHA256 = "hmac:sha2-256";
9
+ const ALLOWED_ATTESTATION_METHOD_PREFIX_HMAC = 0;
10
+
11
+ export class AllowedAttestations {
12
+ async attest(
13
+ allowedAttestationMethod: string,
14
+ actor: string,
15
+ ): Promise<{
16
+ attestation: Uint8Array;
17
+ ticket: Uint8Array;
18
+ }> {
19
+ if (allowedAttestationMethod !== ALLOWED_ATTESTATION_METHOD_HMAC_SHA256) {
20
+ throw new Error(
21
+ `Unsupported allowed attestation method: ${allowedAttestationMethod}`,
22
+ );
23
+ }
24
+
25
+ const ticket = randomBytes();
26
+ const attestation = await hmac(
27
+ sha256,
28
+ ticket,
29
+ new TextEncoder().encode(actor),
30
+ );
31
+
32
+ const prefixedTicket = new Uint8Array(ticket.length + 3);
33
+ prefixedTicket[0] = ALLOWED_ATTESTATION_METHOD_PREFIX_HMAC;
34
+ prefixedTicket[1] = MULTIHASH_CODE_SHA256;
35
+ prefixedTicket[2] = MULTIHASH_LENGTH_SHA256;
36
+ prefixedTicket.set(ticket, 3);
37
+
38
+ return { attestation, ticket: prefixedTicket };
39
+ }
40
+
41
+ async validate(
42
+ attestation: Uint8Array,
43
+ actor: string,
44
+ ticket: Uint8Array,
45
+ ): Promise<boolean> {
46
+ const typePrefix = ticket[0];
47
+ const hashPrefix = ticket[1];
48
+ const lengthPrefix = ticket[2];
49
+ if (
50
+ typePrefix !== ALLOWED_ATTESTATION_METHOD_PREFIX_HMAC ||
51
+ hashPrefix !== MULTIHASH_CODE_SHA256 ||
52
+ lengthPrefix !== MULTIHASH_LENGTH_SHA256
53
+ ) {
54
+ throw new Error(`Unrecognized allowed ticket format`);
55
+ }
56
+
57
+ const expected = await hmac(
58
+ sha256,
59
+ ticket.slice(3),
60
+ new TextEncoder().encode(actor),
61
+ );
62
+
63
+ // Make sure the bytes are exactly equal
64
+ if (attestation.length !== expected.length) {
65
+ return false;
66
+ }
67
+ return expected.every((b, i) => attestation[i] === b);
68
+ }
69
+ }