@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.
- package/README.md +0 -1
- package/dist/browser/ajv-IY2ZY7VT.js +9 -0
- package/dist/browser/ajv-IY2ZY7VT.js.map +7 -0
- package/dist/browser/{chunk-KNUPPOQC.js → chunk-GE6AZATH.js} +2 -2
- package/dist/browser/{chunk-KNUPPOQC.js.map → chunk-GE6AZATH.js.map} +1 -1
- package/dist/browser/{index-browser.es-G37SKL53.js → index-browser.es-UXYPGJ2M.js} +2 -2
- package/dist/browser/{index-browser.es-G37SKL53.js.map → index-browser.es-UXYPGJ2M.js.map} +1 -1
- package/dist/browser/index.js +11 -2
- package/dist/browser/index.js.map +4 -4
- package/dist/cjs/identity.js +112 -0
- package/dist/cjs/identity.js.map +7 -0
- package/dist/cjs/index.js +43 -22
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/media.js +111 -0
- package/dist/cjs/media.js.map +7 -0
- package/dist/cjs/objects.js +307 -0
- package/dist/cjs/objects.js.map +7 -0
- package/dist/cjs/tests.spec.js +1 -2
- package/dist/cjs/tests.spec.js.map +2 -2
- package/dist/cjs/utilities.js +68 -43
- package/dist/cjs/utilities.js.map +2 -2
- package/dist/esm/identity.js +92 -0
- package/dist/esm/identity.js.map +7 -0
- package/dist/esm/index.js +43 -24
- package/dist/esm/index.js.map +2 -2
- package/dist/esm/media.js +91 -0
- package/dist/esm/media.js.map +7 -0
- package/dist/esm/objects.js +285 -0
- package/dist/esm/objects.js.map +7 -0
- package/dist/esm/tests.spec.js +2 -4
- package/dist/esm/tests.spec.js.map +2 -2
- package/dist/esm/utilities.js +69 -48
- package/dist/esm/utilities.js.map +2 -2
- package/dist/{session-manager.d.ts → identity.d.ts} +7 -5
- package/dist/identity.d.ts.map +1 -0
- package/dist/index.d.ts +15 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/media.d.ts +9 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/objects.d.ts +63 -0
- package/dist/objects.d.ts.map +1 -0
- package/dist/utilities.d.ts +19 -8
- package/dist/utilities.d.ts.map +1 -1
- package/package.json +31 -19
- package/src/identity.ts +131 -0
- package/src/index.ts +44 -29
- package/src/media.ts +106 -0
- package/src/objects.ts +431 -0
- package/src/tests.spec.ts +2 -4
- package/src/utilities.ts +67 -87
- package/dist/browser/ajv-6AI3HK2A.js +0 -9
- package/dist/browser/ajv-6AI3HK2A.js.map +0 -7
- package/dist/browser/fast-json-patch-ZE7SZEYK.js +0 -19
- package/dist/browser/fast-json-patch-ZE7SZEYK.js.map +0 -7
- package/dist/cjs/database.js +0 -621
- package/dist/cjs/database.js.map +0 -7
- package/dist/cjs/session-manager.js +0 -107
- package/dist/cjs/session-manager.js.map +0 -7
- package/dist/database.d.ts +0 -105
- package/dist/database.d.ts.map +0 -1
- package/dist/esm/database.js +0 -603
- package/dist/esm/database.js.map +0 -7
- package/dist/esm/session-manager.js +0 -87
- package/dist/esm/session-manager.js.map +0 -7
- package/dist/session-manager.d.ts.map +0 -1
- package/src/database.ts +0 -911
- 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"}
|
package/dist/utilities.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
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
|
package/dist/utilities.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilities.d.ts","sourceRoot":"","sources":["../src/utilities.ts"],"names":[],"mappings":"
|
|
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.
|
|
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
|
-
"./
|
|
20
|
+
"./objects": {
|
|
21
21
|
"import": {
|
|
22
|
-
"types": "./dist/
|
|
23
|
-
"default": "./dist/esm/
|
|
22
|
+
"types": "./dist/objects.d.ts",
|
|
23
|
+
"default": "./dist/esm/objects.js"
|
|
24
24
|
},
|
|
25
25
|
"require": {
|
|
26
|
-
"types": "./dist/
|
|
27
|
-
"default": "./dist/cjs/
|
|
26
|
+
"types": "./dist/objects.d.ts",
|
|
27
|
+
"default": "./dist/cjs/objects.js"
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
|
-
"./
|
|
30
|
+
"./identity": {
|
|
31
31
|
"import": {
|
|
32
|
-
"types": "./dist/
|
|
33
|
-
"default": "./dist/esm/
|
|
32
|
+
"types": "./dist/identity.d.ts",
|
|
33
|
+
"default": "./dist/esm/identity.js"
|
|
34
34
|
},
|
|
35
35
|
"require": {
|
|
36
|
-
"types": "./dist/
|
|
37
|
-
"default": "./dist/cjs/
|
|
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
|
-
"@
|
|
77
|
-
"
|
|
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": "^
|
|
94
|
+
"vitest": "^4.0.16"
|
|
82
95
|
},
|
|
83
96
|
"dependencies": {
|
|
84
|
-
"@graffiti-garden/api": "^0.
|
|
85
|
-
"@types/pouchdb": "^6.4.2",
|
|
97
|
+
"@graffiti-garden/api": "^1.0.1",
|
|
86
98
|
"ajv": "^8.17.1",
|
|
87
|
-
"
|
|
99
|
+
"negotiator": "^1.0.0",
|
|
88
100
|
"pouchdb": "^9.0.0"
|
|
89
101
|
}
|
|
90
102
|
}
|
package/src/identity.ts
ADDED
|
@@ -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 {
|
|
3
|
-
import {
|
|
4
|
-
|
|
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
|
|
18
|
-
protected
|
|
19
|
-
login = this.
|
|
20
|
-
logout = this.
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
channelStats: Graffiti["channelStats"];
|
|
30
|
-
continueObjectStream: Graffiti["continueObjectStream"];
|
|
32
|
+
continueDiscover: Graffiti["continueDiscover"];
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
protected graffitiLocalMedia: GraffitiLocalMedia;
|
|
35
|
+
postMedia: Graffiti["postMedia"];
|
|
36
|
+
getMedia: Graffiti["getMedia"];
|
|
37
|
+
deleteMedia: Graffiti["deleteMedia"];
|
|
34
38
|
|
|
35
|
-
|
|
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.
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|