jinaga-server 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/LICENSE +22 -0
- package/dist/authentication/authentication-device.d.ts +20 -0
- package/dist/authentication/authentication-device.js +61 -0
- package/dist/authentication/authentication-device.js.map +1 -0
- package/dist/authentication/authentication-impl.d.ts +21 -0
- package/dist/authentication/authentication-impl.js +61 -0
- package/dist/authentication/authentication-impl.js.map +1 -0
- package/dist/authentication/authentication-session.d.ts +23 -0
- package/dist/authentication/authentication-session.js +85 -0
- package/dist/authentication/authentication-session.js.map +1 -0
- package/dist/authentication/authentication.d.ts +5 -0
- package/dist/authentication/authentication.js +3 -0
- package/dist/authentication/authentication.js.map +1 -0
- package/dist/authentication.d.ts +17 -0
- package/dist/authentication.js +52 -0
- package/dist/authentication.js.map +1 -0
- package/dist/authorization/authorization-keystore.d.ts +13 -0
- package/dist/authorization/authorization-keystore.js +61 -0
- package/dist/authorization/authorization-keystore.js.map +1 -0
- package/dist/http/node-http.d.ts +7 -0
- package/dist/http/node-http.js +126 -0
- package/dist/http/node-http.js.map +1 -0
- package/dist/http/router.d.ts +20 -0
- package/dist/http/router.js +223 -0
- package/dist/http/router.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/jinaga-server.d.ts +18 -0
- package/dist/jinaga-server.js +101 -0
- package/dist/jinaga-server.js.map +1 -0
- package/dist/keystore.d.ts +9 -0
- package/dist/keystore.js +3 -0
- package/dist/keystore.js.map +1 -0
- package/dist/memory/memory-keystore.d.ts +16 -0
- package/dist/memory/memory-keystore.js +86 -0
- package/dist/memory/memory-keystore.js.map +1 -0
- package/dist/postgres/connection.d.ts +12 -0
- package/dist/postgres/connection.js +83 -0
- package/dist/postgres/connection.js.map +1 -0
- package/dist/postgres/edge-record.d.ts +9 -0
- package/dist/postgres/edge-record.js +30 -0
- package/dist/postgres/edge-record.js.map +1 -0
- package/dist/postgres/maps.d.ts +21 -0
- package/dist/postgres/maps.js +86 -0
- package/dist/postgres/maps.js.map +1 -0
- package/dist/postgres/postgres-keystore.d.ts +20 -0
- package/dist/postgres/postgres-keystore.js +168 -0
- package/dist/postgres/postgres-keystore.js.map +1 -0
- package/dist/postgres/postgres-store.d.ts +20 -0
- package/dist/postgres/postgres-store.js +717 -0
- package/dist/postgres/postgres-store.js.map +1 -0
- package/dist/postgres/query-description-builder.d.ts +13 -0
- package/dist/postgres/query-description-builder.js +104 -0
- package/dist/postgres/query-description-builder.js.map +1 -0
- package/dist/postgres/query-description.d.ts +71 -0
- package/dist/postgres/query-description.js +256 -0
- package/dist/postgres/query-description.js.map +1 -0
- package/dist/postgres/specification-result-sql.d.ts +41 -0
- package/dist/postgres/specification-result-sql.js +260 -0
- package/dist/postgres/specification-result-sql.js.map +1 -0
- package/dist/postgres/specification-sql.d.ts +3 -0
- package/dist/postgres/specification-sql.js +119 -0
- package/dist/postgres/specification-sql.js.map +1 -0
- package/dist/postgres/sql.d.ts +10 -0
- package/dist/postgres/sql.js +396 -0
- package/dist/postgres/sql.js.map +1 -0
- package/dist/util/fn.d.ts +6 -0
- package/dist/util/fn.js +81 -0
- package/dist/util/fn.js.map +1 -0
- package/dist/util/promise.d.ts +1 -0
- package/dist/util/promise.js +10 -0
- package/dist/util/promise.js.map +1 -0
- package/docs/registrations.sql +22 -0
- package/docs/specification.txt +41 -0
- package/docs/sql-generator.gv +25 -0
- package/integration-test/docker-compose.yaml +13 -0
- package/integration-test/jinaga-test/Dockerfile +12 -0
- package/integration-test/jinaga-test/jinaga.test.js +425 -0
- package/integration-test/jinaga-test/package-lock.json +6767 -0
- package/integration-test/jinaga-test/package.json +25 -0
- package/integration-test/postgres/0-init-app-db.sh +16 -0
- package/integration-test/postgres/1-setup.sql +219 -0
- package/integration-test/postgres/2-grant.sh +17 -0
- package/integration-test/postgres/Dockerfile +5 -0
- package/integration-test/run.sh +17 -0
- package/integration-test/start_db.sh +2 -0
- package/integration-test/stop_db.sh +3 -0
- package/integration-test/test.sh +8 -0
- package/interactive/index.js +56 -0
- package/jest.config.js +12 -0
- package/package.json +51 -0
- package/scripts/combined.sql +46 -0
- package/scripts/cte.sql +55 -0
- package/scripts/separate.sql +27 -0
- package/setup.sh +34 -0
- package/setup.sql +329 -0
- package/src/authentication/authentication-device.ts +68 -0
- package/src/authentication/authentication-impl.ts +59 -0
- package/src/authentication/authentication-session.ts +95 -0
- package/src/authentication/authentication.ts +6 -0
- package/src/authentication.ts +45 -0
- package/src/authorization/authorization-keystore.ts +67 -0
- package/src/http/node-http.ts +106 -0
- package/src/http/router.ts +237 -0
- package/src/index.ts +1 -0
- package/src/jinaga-server.ts +125 -0
- package/src/keystore.ts +10 -0
- package/src/memory/memory-keystore.ts +93 -0
- package/src/postgres/connection.ts +73 -0
- package/src/postgres/edge-record.ts +36 -0
- package/src/postgres/maps.ts +89 -0
- package/src/postgres/postgres-keystore.ts +167 -0
- package/src/postgres/postgres-store.ts +874 -0
- package/src/postgres/query-description-builder.ts +116 -0
- package/src/postgres/query-description.ts +385 -0
- package/src/postgres/specification-result-sql.ts +362 -0
- package/src/postgres/specification-sql.ts +138 -0
- package/src/postgres/sql.ts +482 -0
- package/src/util/fn.ts +63 -0
- package/src/util/promise.ts +5 -0
- package/test/authorization/authorizationSpec.ts +209 -0
- package/test/postgres/postgresSpec.ts +385 -0
- package/test/postgres/postgresSpecificationSpec.ts +742 -0
- package/test/reporter.js +25 -0
- package/tsconfig.json +25 -0
- package/upgrade.sh +21 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as http from "http";
|
|
2
|
+
import * as https from "https";
|
|
3
|
+
import { HttpConnection, HttpResponse } from "jinaga";
|
|
4
|
+
import * as url from "url";
|
|
5
|
+
|
|
6
|
+
export class NodeHttpConnection implements HttpConnection {
|
|
7
|
+
constructor(
|
|
8
|
+
private url: string) {
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get(path: string): Promise<{}> {
|
|
12
|
+
return new Promise<{}>((resolve, reject) => {
|
|
13
|
+
const { protocol } = url.parse(this.url + path);
|
|
14
|
+
const get = protocol === "https:" ? https.get : http.get;
|
|
15
|
+
let data = "";
|
|
16
|
+
get(this.url + path, response => {
|
|
17
|
+
response.on("data", chunk => {
|
|
18
|
+
data += chunk;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
response.on("end", () => {
|
|
22
|
+
try {
|
|
23
|
+
const result = JSON.parse(data);
|
|
24
|
+
resolve(result);
|
|
25
|
+
}
|
|
26
|
+
catch (x) {
|
|
27
|
+
reject(x);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}).on("error", err => reject(err));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
post(tail: string, body: {}, timeoutSeconds: number): Promise<HttpResponse> {
|
|
35
|
+
return new Promise<HttpResponse>((resolve, reject) => {
|
|
36
|
+
const { protocol, host, port, path } = url.parse(this.url + tail);
|
|
37
|
+
const request = protocol === "https:" ? https.request : http.request;
|
|
38
|
+
let response = "";
|
|
39
|
+
const req = request({
|
|
40
|
+
method: "POST",
|
|
41
|
+
host, port, path,
|
|
42
|
+
headers: {
|
|
43
|
+
"Accept": "application/json",
|
|
44
|
+
"Content-Type": "application/json"
|
|
45
|
+
},
|
|
46
|
+
timeout: timeoutSeconds * 1000
|
|
47
|
+
}, res => {
|
|
48
|
+
res.on("data", chunk => {
|
|
49
|
+
if (chunk instanceof Buffer) {
|
|
50
|
+
response += chunk.toString();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
response += chunk;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
res.on("error", err => {
|
|
57
|
+
resolve({
|
|
58
|
+
result: "failure",
|
|
59
|
+
error: err.message
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
res.on("end", () => {
|
|
63
|
+
if (res.statusCode === 403) {
|
|
64
|
+
reject(response);
|
|
65
|
+
}
|
|
66
|
+
else if (res.statusCode>= 400) {
|
|
67
|
+
resolve({
|
|
68
|
+
result: "retry",
|
|
69
|
+
error: response
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else if (isJsonResult(res.headers["content-type"])) {
|
|
73
|
+
try {
|
|
74
|
+
const json = JSON.parse(response);
|
|
75
|
+
resolve({
|
|
76
|
+
result: "success",
|
|
77
|
+
response: json
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (x) {
|
|
81
|
+
reject(x);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
reject(response);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
req.on("error", err => {
|
|
91
|
+
resolve({
|
|
92
|
+
result: "failure",
|
|
93
|
+
error: err.message
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
req.write(JSON.stringify(body));
|
|
98
|
+
req.end();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isJsonResult(contentType: string) {
|
|
104
|
+
const json = contentType.split(";").indexOf("application/json");
|
|
105
|
+
return json >= 0;
|
|
106
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Handler, Router } from "express";
|
|
2
|
+
import {
|
|
3
|
+
Authorization,
|
|
4
|
+
Declaration,
|
|
5
|
+
FactRecord,
|
|
6
|
+
Forbidden,
|
|
7
|
+
fromDescriptiveString,
|
|
8
|
+
LoadMessage,
|
|
9
|
+
LoadResponse,
|
|
10
|
+
ProfileMessage,
|
|
11
|
+
QueryMessage,
|
|
12
|
+
QueryResponse,
|
|
13
|
+
SaveMessage,
|
|
14
|
+
SaveResponse,
|
|
15
|
+
SpecificationParser,
|
|
16
|
+
Trace,
|
|
17
|
+
UserIdentity,
|
|
18
|
+
} from "jinaga";
|
|
19
|
+
|
|
20
|
+
function get<U>(method: ((req: RequestUser) => Promise<U>)): Handler {
|
|
21
|
+
return (req, res, next) => {
|
|
22
|
+
const user = <RequestUser>req.user;
|
|
23
|
+
if (!user) {
|
|
24
|
+
res.sendStatus(401);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
method(user)
|
|
28
|
+
.then(response => {
|
|
29
|
+
res.setHeader('Content-Type', 'application/json');
|
|
30
|
+
res.send(JSON.stringify(response));
|
|
31
|
+
next();
|
|
32
|
+
})
|
|
33
|
+
.catch(error => {
|
|
34
|
+
Trace.error(error);
|
|
35
|
+
res.sendStatus(500);
|
|
36
|
+
next();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function post<T, U>(method: (user: RequestUser, message: T) => Promise<U>): Handler {
|
|
43
|
+
return (req, res, next) => {
|
|
44
|
+
const user = <RequestUser>req.user;
|
|
45
|
+
const message = <T>req.body;
|
|
46
|
+
if (!message) {
|
|
47
|
+
throw new Error('Ensure that you have called app.use(express.json()).');
|
|
48
|
+
}
|
|
49
|
+
method(user, message)
|
|
50
|
+
.then(response => {
|
|
51
|
+
res.setHeader('Content-Type', 'application/json');
|
|
52
|
+
res.send(JSON.stringify(response));
|
|
53
|
+
next();
|
|
54
|
+
})
|
|
55
|
+
.catch(error => {
|
|
56
|
+
if (error instanceof Forbidden) {
|
|
57
|
+
Trace.warn(error.message);
|
|
58
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
59
|
+
res.status(403).send(error.message);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
Trace.error(error);
|
|
63
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
64
|
+
res.sendStatus(500).send(error.message);
|
|
65
|
+
}
|
|
66
|
+
next();
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function postString<U>(method: (user: RequestUser, message: string) => Promise<U>): Handler {
|
|
72
|
+
return (req, res, next) => {
|
|
73
|
+
const user = <RequestUser>req.user;
|
|
74
|
+
const input = <string>req.body;
|
|
75
|
+
if (!input || typeof(input) !== 'string') {
|
|
76
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
77
|
+
res.status(500).send('Expected Content-Type text/plain. Ensure that you have called app.use(express.text()).');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
method(user, input)
|
|
81
|
+
.then(response => {
|
|
82
|
+
res.setHeader('Content-Type', 'application/json');
|
|
83
|
+
res.send(JSON.stringify(response, null, 2));
|
|
84
|
+
next();
|
|
85
|
+
})
|
|
86
|
+
.catch(error => {
|
|
87
|
+
if (error instanceof Forbidden) {
|
|
88
|
+
Trace.warn(error.message);
|
|
89
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
90
|
+
res.status(403).send(error.message);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
Trace.error(error);
|
|
94
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
95
|
+
res.status(400).send(error.message);
|
|
96
|
+
}
|
|
97
|
+
next();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function serializeUserIdentity(user: RequestUser) : UserIdentity {
|
|
104
|
+
if (!user) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
provider: user.provider,
|
|
109
|
+
id: user.id
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface RequestUser {
|
|
114
|
+
provider: string;
|
|
115
|
+
id: string;
|
|
116
|
+
profile: ProfileMessage;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export class HttpRouter {
|
|
120
|
+
handler: Handler;
|
|
121
|
+
|
|
122
|
+
constructor(private authorization: Authorization) {
|
|
123
|
+
const router = Router();
|
|
124
|
+
router.get('/login', get(user => this.login(user)));
|
|
125
|
+
router.post('/query', post((user, queryMessage: QueryMessage) => this.query(user, queryMessage)));
|
|
126
|
+
router.post('/load', post((user, loadMessage: LoadMessage) => this.load(user, loadMessage)));
|
|
127
|
+
router.post('/save', post((user, saveMessage: SaveMessage) => this.save(user, saveMessage)));
|
|
128
|
+
|
|
129
|
+
router.post('/read', postString((user, input: string) => this.read(user, input)));
|
|
130
|
+
router.post('/write', post((user, input: string) => this.write(user, input)));
|
|
131
|
+
router.post('/feed', post((user, input: string) => this.feed(user, input)));
|
|
132
|
+
|
|
133
|
+
this.handler = router;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async login(user: RequestUser) {
|
|
137
|
+
const userFact = await this.authorization.getOrCreateUserFact({
|
|
138
|
+
provider: user.provider,
|
|
139
|
+
id: user.id
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
userFact: userFact,
|
|
143
|
+
profile: user.profile
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private async query(user: RequestUser, queryMessage: QueryMessage) : Promise<QueryResponse> {
|
|
148
|
+
const userIdentity = serializeUserIdentity(user);
|
|
149
|
+
const query = fromDescriptiveString(queryMessage.query);
|
|
150
|
+
const result = await this.authorization.query(userIdentity, queryMessage.start, query);
|
|
151
|
+
return {
|
|
152
|
+
results: result
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async load(user: RequestUser, loadMessage: LoadMessage) : Promise<LoadResponse> {
|
|
157
|
+
const userIdentity = serializeUserIdentity(user);
|
|
158
|
+
const result = await this.authorization.load(userIdentity, loadMessage.references);
|
|
159
|
+
return {
|
|
160
|
+
facts: result
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private async save(user: RequestUser, saveMessage: SaveMessage) : Promise<SaveResponse> {
|
|
165
|
+
const userIdentity = serializeUserIdentity(user);
|
|
166
|
+
await this.authorization.save(userIdentity, saveMessage.facts);
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async read(user: RequestUser, input: string): Promise<any[]> {
|
|
171
|
+
const knownFacts = await this.getKnownFacts(user);
|
|
172
|
+
const parser = new SpecificationParser(input);
|
|
173
|
+
parser.skipWhitespace();
|
|
174
|
+
var declaration = parser.parseDeclaration(knownFacts);
|
|
175
|
+
var specification = parser.parseSpecification();
|
|
176
|
+
|
|
177
|
+
// Select starting facts that match the inputs
|
|
178
|
+
const start = specification.given.map(input => {
|
|
179
|
+
const declaredFact = declaration.find(d => d.name === input.name);
|
|
180
|
+
if (!declaredFact) {
|
|
181
|
+
throw new Error(`No fact named ${input.name} was declared`);
|
|
182
|
+
}
|
|
183
|
+
return declaredFact.declared.reference;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const userIdentity = serializeUserIdentity(user);
|
|
187
|
+
const results = await this.authorization.read(userIdentity, start, specification);
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private async write(user: RequestUser, input: string): Promise<SaveResponse> {
|
|
192
|
+
const knownFacts = await this.getKnownFacts(user);
|
|
193
|
+
const parser = new SpecificationParser(input);
|
|
194
|
+
parser.skipWhitespace();
|
|
195
|
+
var declaration = parser.parseDeclaration(knownFacts);
|
|
196
|
+
|
|
197
|
+
const factRecords: FactRecord[] = [];
|
|
198
|
+
for (const value of declaration) {
|
|
199
|
+
if (!value.declared.fact) {
|
|
200
|
+
throw new Error("References are not allowed while saving.");
|
|
201
|
+
}
|
|
202
|
+
factRecords.push(value.declared.fact);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const userIdentity = serializeUserIdentity(user);
|
|
206
|
+
await this.authorization.save(userIdentity, factRecords);
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private feed(user: RequestUser, input: string): Promise<string> {
|
|
211
|
+
throw new Error("Method not implemented.");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async getKnownFacts(user: RequestUser): Promise<Declaration> {
|
|
215
|
+
if (user) {
|
|
216
|
+
const userFact = await this.authorization.getOrCreateUserFact({
|
|
217
|
+
provider: user.provider,
|
|
218
|
+
id: user.id
|
|
219
|
+
});
|
|
220
|
+
return [
|
|
221
|
+
{
|
|
222
|
+
name: "me",
|
|
223
|
+
declared: {
|
|
224
|
+
fact: userFact,
|
|
225
|
+
reference: {
|
|
226
|
+
type: userFact.type,
|
|
227
|
+
hash: userFact.hash
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { JinagaServer, JinagaServerConfig, JinagaServerInstance } from "./jinaga-server";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Handler, Request } from "express";
|
|
2
|
+
import {
|
|
3
|
+
Authorization,
|
|
4
|
+
AuthorizationNoOp,
|
|
5
|
+
AuthorizationRules,
|
|
6
|
+
Cache,
|
|
7
|
+
Feed,
|
|
8
|
+
FeedImpl,
|
|
9
|
+
Fork,
|
|
10
|
+
Jinaga,
|
|
11
|
+
MemoryStore,
|
|
12
|
+
PassThroughFork,
|
|
13
|
+
Storage,
|
|
14
|
+
SyncStatusNotifier,
|
|
15
|
+
TransientFork,
|
|
16
|
+
UserIdentity,
|
|
17
|
+
WebClient,
|
|
18
|
+
} from "jinaga";
|
|
19
|
+
|
|
20
|
+
import { AuthenticationDevice } from "./authentication/authentication-device";
|
|
21
|
+
import { AuthenticationSession } from "./authentication/authentication-session";
|
|
22
|
+
import { AuthorizationKeystore } from "./authorization/authorization-keystore";
|
|
23
|
+
import { NodeHttpConnection } from "./http/node-http";
|
|
24
|
+
import { HttpRouter, RequestUser } from "./http/router";
|
|
25
|
+
import { Keystore } from "./keystore";
|
|
26
|
+
import { PostgresKeystore } from "./postgres/postgres-keystore";
|
|
27
|
+
import { PostgresStore } from "./postgres/postgres-store";
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export type JinagaServerConfig = {
|
|
31
|
+
pgStore?: string,
|
|
32
|
+
pgKeystore?: string,
|
|
33
|
+
httpEndpoint?: string,
|
|
34
|
+
authorization?: (a: AuthorizationRules) => AuthorizationRules,
|
|
35
|
+
httpTimeoutSeconds?: number
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type JinagaServerInstance = {
|
|
39
|
+
handler: Handler,
|
|
40
|
+
j: Jinaga,
|
|
41
|
+
withSession: (req: Request, callback: ((j: Jinaga) => Promise<void>)) => Promise<void>,
|
|
42
|
+
close: () => Promise<void>
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const localDeviceIdentity = {
|
|
46
|
+
provider: 'jinaga',
|
|
47
|
+
id: 'local'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export class JinagaServer {
|
|
51
|
+
static create(config: JinagaServerConfig): JinagaServerInstance {
|
|
52
|
+
const syncStatusNotifier = new SyncStatusNotifier();
|
|
53
|
+
const store = createStore(config);
|
|
54
|
+
const feed = new FeedImpl(store);
|
|
55
|
+
const fork = createFork(config, feed, syncStatusNotifier);
|
|
56
|
+
const keystore = new PostgresKeystore(config.pgKeystore);
|
|
57
|
+
const authorizationRules = config.authorization ? config.authorization(new AuthorizationRules()) : null;
|
|
58
|
+
const authorization = createAuthorization(authorizationRules, fork, keystore);
|
|
59
|
+
const router = new HttpRouter(authorization);
|
|
60
|
+
const authentication = new AuthenticationDevice(fork, keystore, localDeviceIdentity);
|
|
61
|
+
const memory = new MemoryStore();
|
|
62
|
+
const j: Jinaga = new Jinaga(authentication, memory, syncStatusNotifier);
|
|
63
|
+
|
|
64
|
+
async function close() {
|
|
65
|
+
await keystore.close();
|
|
66
|
+
await store.close();
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
handler: router.handler,
|
|
70
|
+
j,
|
|
71
|
+
withSession: (req, callback) => {
|
|
72
|
+
return withSession(feed, keystore, authorizationRules, req, callback);
|
|
73
|
+
},
|
|
74
|
+
close
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createStore(config: JinagaServerConfig): Storage {
|
|
80
|
+
if (config.pgStore) {
|
|
81
|
+
const store = new PostgresStore(config.pgStore);
|
|
82
|
+
const cache = new Cache(store);
|
|
83
|
+
return cache;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return new MemoryStore();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createFork(config: JinagaServerConfig, feed: Feed, syncStatusNotifier: SyncStatusNotifier): Fork {
|
|
91
|
+
if (config.httpEndpoint) {
|
|
92
|
+
const httpConnection = new NodeHttpConnection(config.httpEndpoint);
|
|
93
|
+
const httpTimeoutSeconds = config.httpTimeoutSeconds || 5;
|
|
94
|
+
const webClient = new WebClient(httpConnection, syncStatusNotifier, {
|
|
95
|
+
timeoutSeconds: httpTimeoutSeconds
|
|
96
|
+
});
|
|
97
|
+
const fork = new TransientFork(feed, webClient);
|
|
98
|
+
return fork;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
return new PassThroughFork(feed);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createAuthorization(authorizationRules: AuthorizationRules | null, feed: Feed, keystore: Keystore | null): Authorization {
|
|
106
|
+
if (keystore) {
|
|
107
|
+
const authorization = new AuthorizationKeystore(feed, keystore, authorizationRules);
|
|
108
|
+
return authorization;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
return new AuthorizationNoOp(feed);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function withSession(feed: Feed, keystore: Keystore, authorizationRules: AuthorizationRules | null, req: Request, callback: ((j: Jinaga) => Promise<void>)) {
|
|
116
|
+
const user = <RequestUser>req.user;
|
|
117
|
+
const userIdentity: UserIdentity = {
|
|
118
|
+
provider: user.provider,
|
|
119
|
+
id: user.id
|
|
120
|
+
}
|
|
121
|
+
const authentication = new AuthenticationSession(feed, keystore, authorizationRules, userIdentity, user.profile.displayName, localDeviceIdentity);
|
|
122
|
+
const syncStatusNotifier = new SyncStatusNotifier();
|
|
123
|
+
const j = new Jinaga(authentication, new MemoryStore(), syncStatusNotifier);
|
|
124
|
+
await callback(j);
|
|
125
|
+
}
|
package/src/keystore.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FactEnvelope, FactRecord, UserIdentity } from "jinaga";
|
|
2
|
+
|
|
3
|
+
export interface Keystore {
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
getOrCreateUserFact(userIdentity: UserIdentity): Promise<FactRecord>;
|
|
6
|
+
getOrCreateDeviceFact(userIdentity: UserIdentity): Promise<FactRecord>;
|
|
7
|
+
getUserFact(userIdentity: UserIdentity): Promise<FactRecord>;
|
|
8
|
+
getDeviceFact(userIdentity: UserIdentity): Promise<FactRecord>;
|
|
9
|
+
signFacts(userIdentity: UserIdentity, facts: FactRecord[]): Promise<FactEnvelope[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { computeHash, FactEnvelope, FactRecord, PredecessorCollection, UserIdentity } from "jinaga";
|
|
2
|
+
import { pki } from "node-forge";
|
|
3
|
+
|
|
4
|
+
import { Keystore } from "../keystore";
|
|
5
|
+
|
|
6
|
+
export class MemoryKeystore implements Keystore {
|
|
7
|
+
private keyPairs: { [key: string]: { publicKey: string, privateKey: string }} = {};
|
|
8
|
+
|
|
9
|
+
close(): Promise<void> {
|
|
10
|
+
return Promise.resolve();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getOrCreateUserFact(userIdentity: UserIdentity): Promise<FactRecord> {
|
|
14
|
+
return Promise.resolve(this.getOrCreateIdentityFact('Jinaga.User', userIdentity));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
getOrCreateDeviceFact(userIdentity: UserIdentity): Promise<FactRecord> {
|
|
18
|
+
return Promise.resolve(this.getOrCreateIdentityFact('Jinaga.Device', userIdentity));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getUserFact(userIdentity: UserIdentity): Promise<FactRecord> {
|
|
22
|
+
return Promise.resolve(this.getIdentityFact('Jinaga.User', userIdentity));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getDeviceFact(userIdentity: UserIdentity): Promise<FactRecord> {
|
|
26
|
+
return Promise.resolve(this.getIdentityFact('Jinaga.Device', userIdentity));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
signFacts(userIdentity: UserIdentity, facts: FactRecord[]): Promise<FactEnvelope[]> {
|
|
30
|
+
return Promise.resolve(facts.map(fact => {
|
|
31
|
+
return {
|
|
32
|
+
fact,
|
|
33
|
+
signatures: []
|
|
34
|
+
};
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private getOrCreateIdentityFact(type: string, identity: UserIdentity): FactRecord {
|
|
39
|
+
if (!identity) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const publicKey = this.getOrCreatePublicKey(identity);
|
|
43
|
+
const predecessors: PredecessorCollection = {};
|
|
44
|
+
const fields = {
|
|
45
|
+
publicKey: publicKey
|
|
46
|
+
};
|
|
47
|
+
const hash = computeHash(fields, predecessors);
|
|
48
|
+
return { type, hash, predecessors, fields };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private getIdentityFact(type: string, identity: UserIdentity): FactRecord {
|
|
52
|
+
if (!identity) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const publicKey = this.getPublicKey(identity);
|
|
56
|
+
const predecessors: PredecessorCollection = {};
|
|
57
|
+
const fields = {
|
|
58
|
+
publicKey: publicKey
|
|
59
|
+
};
|
|
60
|
+
const hash = computeHash(fields, predecessors);
|
|
61
|
+
return { type, hash, predecessors, fields };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private getOrCreatePublicKey(userIdentity: UserIdentity): string {
|
|
65
|
+
const key = `${userIdentity.provider}:${userIdentity.id}`;
|
|
66
|
+
const keyPair = this.keyPairs[key];
|
|
67
|
+
if (keyPair) {
|
|
68
|
+
return keyPair.publicKey;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
return this.generateKeyPair(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private getPublicKey(userIdentity: UserIdentity): string {
|
|
76
|
+
const key = `${userIdentity.provider}:${userIdentity.id}`;
|
|
77
|
+
const keyPair = this.keyPairs[key];
|
|
78
|
+
if (keyPair) {
|
|
79
|
+
return keyPair.publicKey;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
throw new Error("Public key not found");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private generateKeyPair(key: string) {
|
|
87
|
+
const keypair = pki.rsa.generateKeyPair({ bits: 1024 });
|
|
88
|
+
const privateKey = pki.privateKeyToPem(keypair.privateKey);
|
|
89
|
+
const publicKey = pki.publicKeyToPem(keypair.publicKey);
|
|
90
|
+
this.keyPairs[key] = { publicKey, privateKey };
|
|
91
|
+
return publicKey;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Pool, PoolClient } from 'pg';
|
|
2
|
+
import { delay } from "../util/promise";
|
|
3
|
+
|
|
4
|
+
export type Row = { [key: string]: any };
|
|
5
|
+
|
|
6
|
+
export class ConnectionFactory {
|
|
7
|
+
private postgresPool: Pool;
|
|
8
|
+
|
|
9
|
+
constructor (postgresUri: string) {
|
|
10
|
+
this.postgresPool = new Pool({
|
|
11
|
+
connectionString: postgresUri
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async close() {
|
|
16
|
+
await this.postgresPool.end();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
withTransaction<T>(callback: (connection: PoolClient) => Promise<T>) {
|
|
20
|
+
return this.with(async connection => {
|
|
21
|
+
try {
|
|
22
|
+
await connection.query('BEGIN');
|
|
23
|
+
const result = await callback(connection);
|
|
24
|
+
await connection.query('COMMIT');
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
await connection.query('ROLLBACK');
|
|
29
|
+
throw e;
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async with<T>(callback: (connection: PoolClient) => Promise<T>) {
|
|
35
|
+
let attempt = 0;
|
|
36
|
+
const pause = [0, 0, 1000, 5000, 15000, 30000];
|
|
37
|
+
while (attempt < pause.length) {
|
|
38
|
+
try {
|
|
39
|
+
const client = await this.createClient();
|
|
40
|
+
try {
|
|
41
|
+
return await callback(client);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
client.release();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
if (!isTransientError(e)) {
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
attempt++;
|
|
52
|
+
if (attempt === pause.length) {
|
|
53
|
+
throw e;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (pause[attempt] > 0) {
|
|
57
|
+
await delay(pause[attempt]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async createClient() {
|
|
63
|
+
return await this.postgresPool.connect();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isTransientError(e: any) {
|
|
68
|
+
if (e.code === 'ECONNREFUSED') {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
console.error("Postgres error:", e);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FactRecord, FactReference } from "jinaga";
|
|
2
|
+
|
|
3
|
+
export type EdgeRecord = {
|
|
4
|
+
predecessor_hash: string,
|
|
5
|
+
predecessor_type: string,
|
|
6
|
+
successor_hash: string,
|
|
7
|
+
successor_type: string,
|
|
8
|
+
role: string
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function makeEdgeRecord(predecessor: FactReference, successor: FactRecord, role: string): EdgeRecord {
|
|
12
|
+
if (!predecessor.hash || !predecessor.type || !successor.hash || !successor.type) {
|
|
13
|
+
throw new Error('Attempting to save edge with null hash or type.');
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
predecessor_hash: predecessor.hash,
|
|
17
|
+
predecessor_type: predecessor.type,
|
|
18
|
+
successor_hash: successor.hash,
|
|
19
|
+
successor_type: successor.type,
|
|
20
|
+
role
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function makeEdgeRecords(fact: FactRecord): EdgeRecord[] {
|
|
25
|
+
let records: EdgeRecord[] = [];
|
|
26
|
+
for (const role in fact.predecessors) {
|
|
27
|
+
const predecessor = fact.predecessors[role];
|
|
28
|
+
if (Array.isArray(predecessor)) {
|
|
29
|
+
records = records.concat(predecessor.map(p => makeEdgeRecord(p, fact, role)));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
records.push(makeEdgeRecord(predecessor, fact, role));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return records;
|
|
36
|
+
}
|