@graffiti-garden/implementation-local 0.6.3 → 1.0.0

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 (67) hide show
  1. package/README.md +0 -1
  2. package/dist/browser/ajv-IY2ZY7VT.js +9 -0
  3. package/dist/browser/ajv-IY2ZY7VT.js.map +7 -0
  4. package/dist/browser/{chunk-KNUPPOQC.js → chunk-GE6AZATH.js} +2 -2
  5. package/dist/browser/{chunk-KNUPPOQC.js.map → chunk-GE6AZATH.js.map} +1 -1
  6. package/dist/browser/{index-browser.es-G37SKL53.js → index-browser.es-UXYPGJ2M.js} +2 -2
  7. package/dist/browser/{index-browser.es-G37SKL53.js.map → index-browser.es-UXYPGJ2M.js.map} +1 -1
  8. package/dist/browser/index.js +11 -2
  9. package/dist/browser/index.js.map +4 -4
  10. package/dist/cjs/identity.js +112 -0
  11. package/dist/cjs/identity.js.map +7 -0
  12. package/dist/cjs/index.js +43 -22
  13. package/dist/cjs/index.js.map +2 -2
  14. package/dist/cjs/media.js +111 -0
  15. package/dist/cjs/media.js.map +7 -0
  16. package/dist/cjs/objects.js +307 -0
  17. package/dist/cjs/objects.js.map +7 -0
  18. package/dist/cjs/tests.spec.js +1 -2
  19. package/dist/cjs/tests.spec.js.map +2 -2
  20. package/dist/cjs/utilities.js +68 -43
  21. package/dist/cjs/utilities.js.map +2 -2
  22. package/dist/esm/identity.js +92 -0
  23. package/dist/esm/identity.js.map +7 -0
  24. package/dist/esm/index.js +43 -24
  25. package/dist/esm/index.js.map +2 -2
  26. package/dist/esm/media.js +91 -0
  27. package/dist/esm/media.js.map +7 -0
  28. package/dist/esm/objects.js +285 -0
  29. package/dist/esm/objects.js.map +7 -0
  30. package/dist/esm/tests.spec.js +2 -4
  31. package/dist/esm/tests.spec.js.map +2 -2
  32. package/dist/esm/utilities.js +69 -48
  33. package/dist/esm/utilities.js.map +2 -2
  34. package/dist/{session-manager.d.ts → identity.d.ts} +7 -5
  35. package/dist/identity.d.ts.map +1 -0
  36. package/dist/index.d.ts +15 -13
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/media.d.ts +9 -0
  39. package/dist/media.d.ts.map +1 -0
  40. package/dist/objects.d.ts +63 -0
  41. package/dist/objects.d.ts.map +1 -0
  42. package/dist/utilities.d.ts +19 -8
  43. package/dist/utilities.d.ts.map +1 -1
  44. package/package.json +31 -19
  45. package/src/identity.ts +131 -0
  46. package/src/index.ts +44 -29
  47. package/src/media.ts +106 -0
  48. package/src/objects.ts +431 -0
  49. package/src/tests.spec.ts +2 -4
  50. package/src/utilities.ts +67 -87
  51. package/dist/browser/ajv-6AI3HK2A.js +0 -9
  52. package/dist/browser/ajv-6AI3HK2A.js.map +0 -7
  53. package/dist/browser/fast-json-patch-ZE7SZEYK.js +0 -19
  54. package/dist/browser/fast-json-patch-ZE7SZEYK.js.map +0 -7
  55. package/dist/cjs/database.js +0 -621
  56. package/dist/cjs/database.js.map +0 -7
  57. package/dist/cjs/session-manager.js +0 -107
  58. package/dist/cjs/session-manager.js.map +0 -7
  59. package/dist/database.d.ts +0 -105
  60. package/dist/database.d.ts.map +0 -1
  61. package/dist/esm/database.js +0 -603
  62. package/dist/esm/database.js.map +0 -7
  63. package/dist/esm/session-manager.js +0 -87
  64. package/dist/esm/session-manager.js.map +0 -7
  65. package/dist/session-manager.d.ts.map +0 -1
  66. package/src/database.ts +0 -911
  67. package/src/session-manager.ts +0 -123
@@ -0,0 +1,63 @@
1
+ import type { Graffiti, JSONSchema, GraffitiSession, GraffitiObjectStreamContinue, GraffitiObjectStreamContinueEntry } from "@graffiti-garden/api";
2
+ import type Ajv from "ajv";
3
+ /**
4
+ * Constructor options for the GraffitiPoubchDB class.
5
+ */
6
+ export interface GraffitiLocalOptions {
7
+ /**
8
+ * Options to pass to the PouchDB constructor.
9
+ * Defaults to `{ name: "graffitiDb" }`.
10
+ *
11
+ * See the [PouchDB documentation](https://pouchdb.com/api.html#create_database)
12
+ * for available options.
13
+ */
14
+ pouchDBOptions?: PouchDB.Configuration.DatabaseConfiguration;
15
+ /**
16
+ * Wait at least this long (in milliseconds) before continuing a stream.
17
+ * A basic form of rate limiting. Defaults to 2 seconds.
18
+ */
19
+ continueBuffer?: number;
20
+ }
21
+ type GraffitiObjectData = {
22
+ tombstone: boolean;
23
+ value: {};
24
+ channels: string[];
25
+ allowed?: string[] | null;
26
+ lastModified: number;
27
+ };
28
+ type ContinueDiscoverParams = {
29
+ lastDiscovered: number;
30
+ ifModifiedSince: number;
31
+ };
32
+ /**
33
+ * An implementation of only the database operations of the
34
+ * GraffitiAPI without synchronization or session management.
35
+ */
36
+ export declare class GraffitiLocalObjects {
37
+ protected db_: Promise<PouchDB.Database<GraffitiObjectData>> | undefined;
38
+ protected ajv_: Promise<Ajv> | undefined;
39
+ protected readonly options: GraffitiLocalOptions;
40
+ protected operationClock: number;
41
+ get db(): Promise<PouchDB.Database<GraffitiObjectData>>;
42
+ protected get ajv(): Promise<Ajv>;
43
+ constructor(options?: GraffitiLocalOptions);
44
+ get: Graffiti["get"];
45
+ delete: Graffiti["delete"];
46
+ post: Graffiti["post"];
47
+ protected discoverMeta<Schema extends JSONSchema>(args: Parameters<typeof Graffiti.prototype.discover<Schema>>, continueParams?: {
48
+ lastDiscovered: number;
49
+ ifModifiedSince: number;
50
+ }): AsyncGenerator<GraffitiObjectStreamContinueEntry<Schema>, ContinueDiscoverParams>;
51
+ protected discoverCursor(args: Parameters<typeof Graffiti.prototype.discover<{}>>, continueParams: {
52
+ lastDiscovered: number;
53
+ ifModifiedSince: number;
54
+ }): string;
55
+ protected discoverContinue<Schema extends JSONSchema>(args: Parameters<typeof Graffiti.prototype.discover<Schema>>, continueParams: {
56
+ lastDiscovered: number;
57
+ ifModifiedSince: number;
58
+ }, session?: GraffitiSession | null): GraffitiObjectStreamContinue<Schema>;
59
+ discover: Graffiti["discover"];
60
+ continueDiscover: Graffiti["continueDiscover"];
61
+ }
62
+ export {};
63
+ //# sourceMappingURL=objects.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"objects.d.ts","sourceRoot":"","sources":["../src/objects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EAER,UAAU,EACV,eAAe,EACf,4BAA4B,EAC5B,iCAAiC,EAClC,MAAM,sBAAsB,CAAC;AAW9B,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAE3B;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAC;IAC7D;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,KAAK,kBAAkB,GAAG;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,EAAE,CAAC;IACV,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,sBAAsB,GAAG;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;GAGG;AACH,qBAAa,oBAAoB;IAC/B,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC,GAAG,SAAS,CAAC;IACzE,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACzC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACjD,SAAS,CAAC,cAAc,EAAE,MAAM,CAAK;IAErC,IAAI,EAAE,kDAkDL;IAED,SAAS,KAAK,GAAG,iBAQhB;gBAEW,OAAO,CAAC,EAAE,oBAAoB;IAI1C,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CA4ClB;IAEF,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,CAiCxB;IAEF,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CA6BpB;cAEe,YAAY,CAAC,MAAM,SAAS,UAAU,EACrD,IAAI,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAC5D,cAAc,CAAC,EAAE;QACf,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;KACzB,GACA,cAAc,CACf,iCAAiC,CAAC,MAAM,CAAC,EACzC,sBAAsB,CACvB;IAoFD,SAAS,CAAC,cAAc,CACtB,IAAI,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,EACxD,cAAc,EAAE;QACd,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;KACzB,GACA,MAAM;cAaQ,gBAAgB,CAAC,MAAM,SAAS,UAAU,EACzD,IAAI,EAAE,UAAU,CAAC,OAAO,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAC5D,cAAc,EAAE;QACd,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;KACzB,EACD,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,GAC/B,4BAA4B,CAAC,MAAM,CAAC;IAqBvC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CA4B5B;IAEF,gBAAgB,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAmB5C;CACH"}
@@ -1,10 +1,21 @@
1
- import type { GraffitiObject, GraffitiObjectBase, GraffitiPatch, JSONSchema, GraffitiSession, GraffitiObjectUrl } from "@graffiti-garden/api";
2
- import type { Ajv } from "ajv";
3
- import type { applyPatch } from "fast-json-patch";
4
- export declare function unpackObjectUrl(url: string | GraffitiObjectUrl): string;
1
+ export declare function encodeBase64(bytes: Uint8Array): string;
2
+ export declare function decodeBase64(base64Url: string): Uint8Array;
5
3
  export declare function randomBase64(numBytes?: number): string;
6
- export declare function applyGraffitiPatch<Prop extends keyof GraffitiPatch>(apply: typeof applyPatch, prop: Prop, patch: GraffitiPatch, object: GraffitiObjectBase): void;
7
- export declare function compileGraffitiObjectSchema<Schema extends JSONSchema>(ajv: Ajv, schema: Schema): (data: GraffitiObjectBase) => data is GraffitiObject<Schema>;
8
- export declare function maskGraffitiObject(object: GraffitiObjectBase, channels: string[], session?: GraffitiSession | null): void;
9
- export declare function isActorAllowedGraffitiObject(object: GraffitiObjectBase, session?: GraffitiSession | null): boolean;
4
+ export declare function encodeGraffitiUrl(actor: string, id: string, prefix: string): string;
5
+ export declare function encodeObjectUrl(actor: string, id: string): string;
6
+ export declare function encodeMediaUrl(actor: string, id: string): string;
7
+ export declare function decodeGraffitiUrl(url: string, prefix: string): {
8
+ actor: string;
9
+ id: string;
10
+ };
11
+ export declare function decodeObjectUrl(url: string): {
12
+ actor: string;
13
+ id: string;
14
+ };
15
+ export declare function decodeMediaUrl(url: string): {
16
+ actor: string;
17
+ id: string;
18
+ };
19
+ export declare function blobToBase64(blob: Blob): Promise<string>;
20
+ export declare function base64ToBlob(dataUrl: string): Promise<Blob>;
10
21
  //# sourceMappingURL=utilities.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,UAAU,EACV,eAAe,EACf,iBAAiB,EAClB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,UAE9D;AAED,wBAAgB,YAAY,CAAC,QAAQ,GAAE,MAAW,UAOjD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,SAAS,MAAM,aAAa,EACjE,KAAK,EAAE,OAAO,UAAU,EACxB,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,aAAa,EACpB,MAAM,EAAE,kBAAkB,GACzB,IAAI,CAuBN;AAED,wBAAgB,2BAA2B,CAAC,MAAM,SAAS,UAAU,EACnE,GAAG,EAAE,GAAG,EACR,MAAM,EAAE,MAAM,UAQJ,kBAAkB,KACrB,IAAI,IAAI,cAAc,CAAC,MAAM,CAAC,CAMtC;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,EAAE,EAClB,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,GAC/B,IAAI,CAON;AACD,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,kBAAkB,EAC1B,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,WASjC"}
1
+ {"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAKtD;AAED,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAO1D;AAED,wBAAgB,YAAY,CAAC,QAAQ,GAAE,MAAW,GAAG,MAAM,CAK1D;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,UAE1E;AACD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,UAExD;AACD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,UAEvD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;;;EAU5D;AACD,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM;;;EAE1C;AACD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM;;;EAEzC;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAsB9D;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,iBAGjD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graffiti-garden/implementation-local",
3
- "version": "0.6.3",
3
+ "version": "1.0.0",
4
4
  "description": "A local implementation of the Graffiti API using PouchDB",
5
5
  "types": "./dist/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
@@ -17,24 +17,34 @@
17
17
  "default": "./dist/cjs/index.js"
18
18
  }
19
19
  },
20
- "./database": {
20
+ "./objects": {
21
21
  "import": {
22
- "types": "./dist/database.d.ts",
23
- "default": "./dist/esm/database.js"
22
+ "types": "./dist/objects.d.ts",
23
+ "default": "./dist/esm/objects.js"
24
24
  },
25
25
  "require": {
26
- "types": "./dist/database.d.ts",
27
- "default": "./dist/cjs/database.js"
26
+ "types": "./dist/objects.d.ts",
27
+ "default": "./dist/cjs/objects.js"
28
28
  }
29
29
  },
30
- "./session-manager": {
30
+ "./identity": {
31
31
  "import": {
32
- "types": "./dist/session-manager.d.ts",
33
- "default": "./dist/esm/session-manager.js"
32
+ "types": "./dist/identity.d.ts",
33
+ "default": "./dist/esm/identity.js"
34
34
  },
35
35
  "require": {
36
- "types": "./dist/session-manager.d.ts",
37
- "default": "./dist/cjs/session-manager.js"
36
+ "types": "./dist/identity.d.ts",
37
+ "default": "./dist/cjs/identity.js"
38
+ }
39
+ },
40
+ "./media": {
41
+ "import": {
42
+ "types": "./dist/media.d.ts",
43
+ "default": "./dist/esm/media.js"
44
+ },
45
+ "require": {
46
+ "types": "./dist/media.d.ts",
47
+ "default": "./dist/cjs/media.js"
38
48
  }
39
49
  },
40
50
  "./utilities": {
@@ -49,8 +59,8 @@
49
59
  }
50
60
  },
51
61
  "scripts": {
52
- "test": "vitest run",
53
- "test:watch": "vitest",
62
+ "test": "vitest run src/tests.spec.ts",
63
+ "test:watch": "vitest --watch",
54
64
  "test:coverage": "vitest --coverage",
55
65
  "build:types": "tsc --declaration --emitDeclarationOnly",
56
66
  "build:js": "tsx esbuild.config.mts",
@@ -73,18 +83,20 @@
73
83
  "url": "https://github.com/graffiti-garden/implementation-local/issues"
74
84
  },
75
85
  "devDependencies": {
76
- "@vitest/coverage-v8": "^3.0.6",
77
- "esbuild": "^0.25.0",
86
+ "@types/negotiator": "^0.6.4",
87
+ "@types/node": "^25.0.3",
88
+ "@types/pouchdb": "^6.4.2",
89
+ "@vitest/coverage-v8": "^4.0.16",
90
+ "esbuild": "^0.27.2",
78
91
  "esbuild-plugin-polyfill-node": "^0.3.0",
79
92
  "tsx": "^4.19.2",
80
93
  "typescript": "^5.7.3",
81
- "vitest": "^3.0.5"
94
+ "vitest": "^4.0.16"
82
95
  },
83
96
  "dependencies": {
84
- "@graffiti-garden/api": "^0.6.3",
85
- "@types/pouchdb": "^6.4.2",
97
+ "@graffiti-garden/api": "^1.0.1",
86
98
  "ajv": "^8.17.1",
87
- "fast-json-patch": "^3.1.1",
99
+ "negotiator": "^1.0.0",
88
100
  "pouchdb": "^9.0.0"
89
101
  }
90
102
  }
@@ -0,0 +1,131 @@
1
+ import type {
2
+ Graffiti,
3
+ GraffitiLoginEvent,
4
+ GraffitiLogoutEvent,
5
+ GraffitiSessionInitializedEvent,
6
+ } from "@graffiti-garden/api";
7
+ import { decodeBase64, encodeBase64 } from "./utilities";
8
+
9
+ const DID_LOCAL_PREFIX = "did:local:";
10
+
11
+ /**
12
+ * A class that implements the login methods
13
+ * of the [Graffiti API]() for use in the browser.
14
+ * It is completely insecure and should only be used
15
+ * for testing and demonstrations.
16
+ *
17
+ * It uses `localStorage` to store login state and
18
+ * window prompts rather than an oauth flow for log in.
19
+ * It can be used in node.js but will not persist
20
+ * login state and a proposed username must be provided.
21
+ */
22
+ export class GraffitiLocalIdentity {
23
+ sessionEvents: Graffiti["sessionEvents"] = new EventTarget();
24
+
25
+ handleToActor: Graffiti["handleToActor"] = async (handle: string) => {
26
+ const bytes = new TextEncoder().encode(handle);
27
+ const base64 = encodeBase64(bytes);
28
+ return `${DID_LOCAL_PREFIX}${base64}`;
29
+ };
30
+
31
+ actorToHandle: Graffiti["actorToHandle"] = async (actor: string) => {
32
+ if (!actor.startsWith(DID_LOCAL_PREFIX)) {
33
+ throw new Error(`actor must start with ${DID_LOCAL_PREFIX}`);
34
+ }
35
+ const base64 = actor.slice(DID_LOCAL_PREFIX.length);
36
+ const bytes = decodeBase64(base64);
37
+ return new TextDecoder().decode(bytes);
38
+ };
39
+
40
+ constructor() {
41
+ // Look for any existing sessions
42
+ const sessionRestorer = async () => {
43
+ // Allow listeners to be added first
44
+ await Promise.resolve();
45
+
46
+ // Restore previous sessions
47
+ for (const handle of this.getLoggedInHandles()) {
48
+ const event: GraffitiLoginEvent = new CustomEvent("login", {
49
+ detail: { session: { actor: await this.handleToActor(handle) } },
50
+ });
51
+ this.sessionEvents.dispatchEvent(event);
52
+ }
53
+
54
+ const event: GraffitiSessionInitializedEvent = new CustomEvent(
55
+ "initialized",
56
+ { detail: {} },
57
+ );
58
+ this.sessionEvents.dispatchEvent(event);
59
+ };
60
+ sessionRestorer();
61
+ }
62
+
63
+ loggedInHandles: string[] = [];
64
+
65
+ protected getLoggedInHandles(): string[] {
66
+ if (typeof window !== "undefined") {
67
+ const handlesString = window.localStorage.getItem("graffiti-handles");
68
+ return handlesString
69
+ ? handlesString.split(",").map(decodeURIComponent)
70
+ : [];
71
+ } else {
72
+ return this.loggedInHandles;
73
+ }
74
+ }
75
+
76
+ protected setLoggedInHandles(handles: string[]) {
77
+ if (typeof window !== "undefined") {
78
+ window.localStorage.setItem("graffiti-handles", handles.join(","));
79
+ } else {
80
+ this.loggedInHandles = handles;
81
+ }
82
+ }
83
+
84
+ login: Graffiti["login"] = async (actor) => {
85
+ // Wait a tick for the browser to update the UI
86
+ await new Promise((resolve) => setTimeout(resolve, 0));
87
+
88
+ let handle = actor ? await this.actorToHandle(actor) : undefined;
89
+
90
+ if (typeof window !== "undefined") {
91
+ const response = window.prompt("Choose a username to log in.", handle);
92
+ handle = response ?? undefined;
93
+ }
94
+
95
+ if (!handle) {
96
+ const detail: GraffitiLoginEvent["detail"] = {
97
+ error: new Error("No handle provided to login"),
98
+ };
99
+ const event: GraffitiLoginEvent = new CustomEvent("login", { detail });
100
+ this.sessionEvents.dispatchEvent(event);
101
+ } else {
102
+ const existingHandles = this.getLoggedInHandles();
103
+ if (!existingHandles.includes(handle)) {
104
+ this.setLoggedInHandles([...existingHandles, handle]);
105
+ }
106
+ // Refresh the page to simulate oauth
107
+ window.location.reload();
108
+ }
109
+ };
110
+
111
+ logout: Graffiti["logout"] = async (session) => {
112
+ const handle = await this.actorToHandle(session.actor);
113
+ const existingHandles = this.getLoggedInHandles();
114
+ const exists = existingHandles.includes(handle);
115
+ if (exists) {
116
+ this.setLoggedInHandles(existingHandles.filter((h) => h !== handle));
117
+ }
118
+
119
+ const detail: GraffitiLogoutEvent["detail"] = exists
120
+ ? {
121
+ actor: session.actor,
122
+ }
123
+ : {
124
+ actor: session.actor,
125
+ error: new Error("Not logged in with that actor"),
126
+ };
127
+
128
+ const event: GraffitiLogoutEvent = new CustomEvent("logout", { detail });
129
+ this.sessionEvents.dispatchEvent(event);
130
+ };
131
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import { Graffiti, type GraffitiSession } from "@graffiti-garden/api";
2
- import { GraffitiLocalSessionManager } from "./session-manager.js";
3
- import {
4
- GraffitiLocalDatabase,
5
- type GraffitiLocalOptions,
6
- } from "./database.js";
2
+ import { GraffitiLocalIdentity } from "./identity";
3
+ import { GraffitiLocalObjects, type GraffitiLocalOptions } from "./objects";
4
+ import { GraffitiLocalMedia } from "./media";
7
5
 
8
6
  export type { GraffitiLocalOptions };
9
7
 
@@ -14,36 +12,53 @@ export type { GraffitiLocalOptions };
14
12
  * It can also be configured to work with an external [CouchDB](https://couchdb.apache.org/) server,
15
13
  * although using it with a remote server will not be secure.
16
14
  */
17
- export class GraffitiLocal extends Graffiti {
18
- protected sessionManagerLocal = new GraffitiLocalSessionManager();
19
- login = this.sessionManagerLocal.login.bind(this.sessionManagerLocal);
20
- logout = this.sessionManagerLocal.logout.bind(this.sessionManagerLocal);
21
- sessionEvents = this.sessionManagerLocal.sessionEvents;
15
+ export class GraffitiLocal implements Graffiti {
16
+ protected graffitiLocalIdentity = new GraffitiLocalIdentity();
17
+ login = this.graffitiLocalIdentity.login.bind(this.graffitiLocalIdentity);
18
+ logout = this.graffitiLocalIdentity.logout.bind(this.graffitiLocalIdentity);
19
+ handleToActor = this.graffitiLocalIdentity.handleToActor.bind(
20
+ this.graffitiLocalIdentity,
21
+ );
22
+ actorToHandle = this.graffitiLocalIdentity.actorToHandle.bind(
23
+ this.graffitiLocalIdentity,
24
+ );
25
+ sessionEvents = this.graffitiLocalIdentity.sessionEvents;
22
26
 
23
- put: Graffiti["put"];
27
+ protected graffitiLocalObjects: GraffitiLocalObjects;
28
+ post: Graffiti["post"];
24
29
  get: Graffiti["get"];
25
- patch: Graffiti["patch"];
26
30
  delete: Graffiti["delete"];
27
31
  discover: Graffiti["discover"];
28
- recoverOrphans: Graffiti["recoverOrphans"];
29
- channelStats: Graffiti["channelStats"];
30
- continueObjectStream: Graffiti["continueObjectStream"];
32
+ continueDiscover: Graffiti["continueDiscover"];
31
33
 
32
- constructor(options?: GraffitiLocalOptions) {
33
- super();
34
+ protected graffitiLocalMedia: GraffitiLocalMedia;
35
+ postMedia: Graffiti["postMedia"];
36
+ getMedia: Graffiti["getMedia"];
37
+ deleteMedia: Graffiti["deleteMedia"];
34
38
 
35
- const graffitiPouchDbBase = new GraffitiLocalDatabase(options);
39
+ constructor(options?: GraffitiLocalOptions) {
40
+ this.graffitiLocalObjects = new GraffitiLocalObjects(options);
41
+ this.post = this.graffitiLocalObjects.post.bind(this.graffitiLocalObjects);
42
+ this.get = this.graffitiLocalObjects.get.bind(this.graffitiLocalObjects);
43
+ this.delete = this.graffitiLocalObjects.delete.bind(
44
+ this.graffitiLocalObjects,
45
+ );
46
+ this.discover = this.graffitiLocalObjects.discover.bind(
47
+ this.graffitiLocalObjects,
48
+ );
49
+ this.continueDiscover = this.graffitiLocalObjects.continueDiscover.bind(
50
+ this.graffitiLocalObjects,
51
+ );
36
52
 
37
- this.put = graffitiPouchDbBase.put.bind(graffitiPouchDbBase);
38
- this.get = graffitiPouchDbBase.get.bind(graffitiPouchDbBase);
39
- this.patch = graffitiPouchDbBase.patch.bind(graffitiPouchDbBase);
40
- this.delete = graffitiPouchDbBase.delete.bind(graffitiPouchDbBase);
41
- this.discover = graffitiPouchDbBase.discover.bind(graffitiPouchDbBase);
42
- this.recoverOrphans =
43
- graffitiPouchDbBase.recoverOrphans.bind(graffitiPouchDbBase);
44
- this.channelStats =
45
- graffitiPouchDbBase.channelStats.bind(graffitiPouchDbBase);
46
- this.continueObjectStream =
47
- graffitiPouchDbBase.continueObjectStream.bind(graffitiPouchDbBase);
53
+ this.graffitiLocalMedia = new GraffitiLocalMedia(this.graffitiLocalObjects);
54
+ this.postMedia = this.graffitiLocalMedia.postMedia.bind(
55
+ this.graffitiLocalMedia,
56
+ );
57
+ this.getMedia = this.graffitiLocalMedia.getMedia.bind(
58
+ this.graffitiLocalMedia,
59
+ );
60
+ this.deleteMedia = this.graffitiLocalMedia.deleteMedia.bind(
61
+ this.graffitiLocalMedia,
62
+ );
48
63
  }
49
64
  }
package/src/media.ts ADDED
@@ -0,0 +1,106 @@
1
+ import {
2
+ GraffitiErrorNotAcceptable,
3
+ GraffitiErrorTooLarge,
4
+ type Graffiti,
5
+ type JSONSchema,
6
+ } from "@graffiti-garden/api";
7
+ import {
8
+ decodeObjectUrl,
9
+ encodeObjectUrl,
10
+ decodeMediaUrl,
11
+ encodeMediaUrl,
12
+ blobToBase64,
13
+ base64ToBlob,
14
+ } from "./utilities";
15
+ import Negotiator from "negotiator";
16
+
17
+ const MEDIA_OBJECT_SCHEMA = {
18
+ properties: {
19
+ value: {
20
+ properties: {
21
+ dataBase64: { type: "string" },
22
+ type: { type: "string" },
23
+ size: { type: "number" },
24
+ },
25
+ required: ["dataBase64", "type", "size"],
26
+ },
27
+ },
28
+ } as const satisfies JSONSchema;
29
+
30
+ export class GraffitiLocalMedia {
31
+ protected db: Pick<Graffiti, "post" | "get" | "delete">;
32
+
33
+ constructor(db: Pick<Graffiti, "post" | "get" | "delete">) {
34
+ this.db = db;
35
+ }
36
+
37
+ postMedia: Graffiti["postMedia"] = async (...args) => {
38
+ const [media, session] = args;
39
+
40
+ const dataBase64 = await blobToBase64(media.data);
41
+ const type = media.data.type;
42
+
43
+ const { url } = await this.db.post<typeof MEDIA_OBJECT_SCHEMA>(
44
+ {
45
+ value: {
46
+ dataBase64,
47
+ type,
48
+ size: media.data.size,
49
+ },
50
+ channels: [],
51
+ allowed: media.allowed,
52
+ },
53
+ session,
54
+ );
55
+
56
+ const { actor, id } = decodeObjectUrl(url);
57
+ return encodeMediaUrl(actor, id);
58
+ };
59
+
60
+ getMedia: Graffiti["getMedia"] = async (...args) => {
61
+ const [mediaUrl, requirements, session] = args;
62
+ const { actor, id } = decodeMediaUrl(mediaUrl);
63
+ const objectUrl = encodeObjectUrl(actor, id);
64
+
65
+ const object = await this.db.get<typeof MEDIA_OBJECT_SCHEMA>(
66
+ objectUrl,
67
+ MEDIA_OBJECT_SCHEMA,
68
+ session,
69
+ );
70
+
71
+ const { dataBase64, type, size } = object.value;
72
+
73
+ if (requirements?.maxBytes && size > requirements.maxBytes) {
74
+ throw new GraffitiErrorTooLarge("File size exceeds limit");
75
+ }
76
+
77
+ // Make sure it adheres to requirements.accept
78
+ if (requirements?.accept) {
79
+ const negotiator = new Negotiator({
80
+ headers: { accept: requirements.accept },
81
+ });
82
+ if (negotiator.mediaType([type]) !== type) {
83
+ throw new GraffitiErrorNotAcceptable(`Unsupported media type, ${type}`);
84
+ }
85
+ }
86
+
87
+ const data = await base64ToBlob(dataBase64);
88
+ if (data.size !== size || data.type !== type) {
89
+ throw new Error("Invalid data");
90
+ }
91
+
92
+ return {
93
+ data,
94
+ actor: object.actor,
95
+ allowed: object.allowed,
96
+ };
97
+ };
98
+
99
+ deleteMedia: Graffiti["deleteMedia"] = async (...args) => {
100
+ const [mediaUrl, session] = args;
101
+ const { actor, id } = decodeMediaUrl(mediaUrl);
102
+ const objectUrl = encodeObjectUrl(actor, id);
103
+
104
+ await this.db.delete(objectUrl, session);
105
+ };
106
+ }