@webqit/webflo 1.0.20 → 1.0.22
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/package.json +4 -2
- package/src/config-pi/runtime/Client.js +5 -5
- package/src/config-pi/runtime/Server.js +49 -9
- package/src/runtime-pi/HttpUser.js +3 -78
- package/src/runtime-pi/WebfloCookieStorage.js +25 -9
- package/src/runtime-pi/WebfloRuntime.js +2 -2
- package/src/runtime-pi/WebfloStorage.js +67 -41
- package/src/runtime-pi/client/Capabilities.js +7 -7
- package/src/runtime-pi/client/CookieStorage.js +1 -1
- package/src/runtime-pi/client/SessionStorage.js +8 -25
- package/src/runtime-pi/client/worker/CookieStorage.js +5 -3
- package/src/runtime-pi/client/worker/SessionStorage.js +2 -6
- package/src/runtime-pi/client/worker/WebfloWorker.js +1 -1
- package/src/runtime-pi/server/CookieStorage.js +5 -3
- package/src/runtime-pi/server/SessionStorage.js +16 -18
- package/src/runtime-pi/server/WebfloServer.js +62 -14
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"vanila-javascript"
|
|
13
13
|
],
|
|
14
14
|
"homepage": "https://webqit.io/tooling/webflo",
|
|
15
|
-
"version": "1.0.
|
|
15
|
+
"version": "1.0.22",
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
@@ -35,15 +35,17 @@
|
|
|
35
35
|
"webflo-certbot-http-cleanup-hook": "src/services-pi/cert/http-cleanup-hook.js"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@linked-db/linked-ql": "^0.30.13",
|
|
38
39
|
"@octokit/webhooks": "^7.15.1",
|
|
39
40
|
"@webqit/backpack": "^0.1.8",
|
|
40
41
|
"@webqit/observer": "^2.0.7",
|
|
41
42
|
"@webqit/oohtml-ssr": "^2.1.1",
|
|
42
43
|
"@webqit/util": "^0.8.11",
|
|
43
|
-
"client-sessions": "^0.8.0",
|
|
44
44
|
"esbuild": "^0.14.38",
|
|
45
|
+
"ioredis": "^5.5.0",
|
|
45
46
|
"jsdom": "^21.1.1",
|
|
46
47
|
"mime-types": "^2.1.33",
|
|
48
|
+
"pg": "^8.13.3",
|
|
47
49
|
"simple-git": "^2.20.1",
|
|
48
50
|
"stream-slice": "^0.1.2",
|
|
49
51
|
"urlpattern-polyfill": "^4.0.3",
|
|
@@ -28,8 +28,8 @@ export default class Client extends Dotfile {
|
|
|
28
28
|
webpush: false,
|
|
29
29
|
custom_install: false,
|
|
30
30
|
exposed: ['display-mode', 'notifications'],
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
vapid_public_key_variable: 'VAPID_PUBLIC_KEY',
|
|
32
|
+
generic_public_webhook_url_variable: 'GENERIC_PUBLIC_WEBHOOK_URL',
|
|
33
33
|
},
|
|
34
34
|
}, config, 'patch');
|
|
35
35
|
}
|
|
@@ -104,14 +104,14 @@ export default class Client extends Dotfile {
|
|
|
104
104
|
initial: (config.exposed || []).join(', '),
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
|
-
name: '
|
|
107
|
+
name: 'vapid_public_key_variable',
|
|
108
108
|
type: (prev, answers) => !answers.webpush ? null : 'text',
|
|
109
109
|
message: 'Enter the environment variable name for APP_VAPID_PUBLIC_KEY if not as written',
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
|
-
name: '
|
|
112
|
+
name: 'generic_public_webhook_url_variable',
|
|
113
113
|
type: 'text',
|
|
114
|
-
message: 'Enter the environment variable name for
|
|
114
|
+
message: 'Enter the environment variable name for GENERIC_PUBLIC_WEBHOOK_URL if not as written',
|
|
115
115
|
},
|
|
116
116
|
]
|
|
117
117
|
}
|
|
@@ -29,11 +29,17 @@ export default class Server extends Dotfile {
|
|
|
29
29
|
force: false,
|
|
30
30
|
},
|
|
31
31
|
force_www: '',
|
|
32
|
-
session_key_variable: '
|
|
32
|
+
session_key_variable: 'SESSION_KEY',
|
|
33
33
|
capabilities: {
|
|
34
|
+
database: false,
|
|
35
|
+
database_dialect: 'postgres',
|
|
36
|
+
database_url_variable: 'DATABASE_URL',
|
|
37
|
+
redis: false,
|
|
38
|
+
redis_url_variable: 'REDIS_URL',
|
|
34
39
|
webpush: false,
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
vapid_subject: 'mailto:foo@example.com',
|
|
41
|
+
vapid_public_key_variable: 'VAPID_PUBLIC_KEY',
|
|
42
|
+
vapid_private_key_variable: 'VAPID_PRIVATE_KEY',
|
|
37
43
|
},
|
|
38
44
|
}, config, 'patch');
|
|
39
45
|
}
|
|
@@ -74,7 +80,7 @@ export default class Server extends Dotfile {
|
|
|
74
80
|
{
|
|
75
81
|
name: 'session_key_variable',
|
|
76
82
|
type: 'text',
|
|
77
|
-
message: 'Enter the environment variable name for
|
|
83
|
+
message: 'Enter the environment variable name for SESSION_KEY if not as written',
|
|
78
84
|
initial: config.session_key_variable,
|
|
79
85
|
},
|
|
80
86
|
{
|
|
@@ -124,22 +130,56 @@ export default class Server extends Dotfile {
|
|
|
124
130
|
},
|
|
125
131
|
initial: config.capabilities,
|
|
126
132
|
schema: [
|
|
133
|
+
{
|
|
134
|
+
name: 'database',
|
|
135
|
+
type: 'toggle',
|
|
136
|
+
message: 'Add database integration?',
|
|
137
|
+
active: 'YES',
|
|
138
|
+
inactive: 'NO',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'database_dialect',
|
|
142
|
+
type: (prev, answers) => !answers.database ? null : 'text',
|
|
143
|
+
message: 'Enter the database dialect (postgres for now)',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'database_url_variable',
|
|
147
|
+
type: (prev, answers) => !answers.database ? null : 'text',
|
|
148
|
+
message: 'Enter the environment variable name for DATABASE_URL if not as written',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'redis',
|
|
152
|
+
type: 'toggle',
|
|
153
|
+
message: 'Add redis integration?',
|
|
154
|
+
active: 'YES',
|
|
155
|
+
inactive: 'NO',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: 'redis_url_variable',
|
|
159
|
+
type: (prev, answers) => !answers.redis ? null : 'text',
|
|
160
|
+
message: 'Enter the environment variable name for REDIS_URL if not as written',
|
|
161
|
+
},
|
|
127
162
|
{
|
|
128
163
|
name: 'webpush',
|
|
129
164
|
type: 'toggle',
|
|
130
|
-
message: '
|
|
165
|
+
message: 'Add webpush integration?',
|
|
131
166
|
active: 'YES',
|
|
132
167
|
inactive: 'NO',
|
|
133
168
|
},
|
|
134
169
|
{
|
|
135
|
-
name: '
|
|
170
|
+
name: 'vapid_subject',
|
|
171
|
+
type: (prev, answers) => !answers.webpush ? null : 'text',
|
|
172
|
+
message: 'Enter the vapid_subject URL',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'vapid_public_key_variable',
|
|
136
176
|
type: (prev, answers) => !answers.webpush ? null : 'text',
|
|
137
|
-
message: 'Enter the environment variable name for
|
|
177
|
+
message: 'Enter the environment variable name for VAPID_PUBLIC_KEY if not as written',
|
|
138
178
|
},
|
|
139
179
|
{
|
|
140
|
-
name: '
|
|
180
|
+
name: 'vapid_private_key_variable',
|
|
141
181
|
type: (prev, answers) => !answers.webpush ? null : 'text',
|
|
142
|
-
message: 'Enter the environment variable name for
|
|
182
|
+
message: 'Enter the environment variable name for VAPID_PRIVATE_KEY if not as written',
|
|
143
183
|
},
|
|
144
184
|
]
|
|
145
185
|
},
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { _isObject } from '@webqit/util/js/index.js';
|
|
2
1
|
import { WebfloStorage } from './WebfloStorage.js';
|
|
3
2
|
|
|
4
3
|
export class HttpUser extends WebfloStorage {
|
|
@@ -7,89 +6,15 @@ export class HttpUser extends WebfloStorage {
|
|
|
7
6
|
return new this(request, session, client);
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
#session;
|
|
11
9
|
#client;
|
|
12
10
|
|
|
13
11
|
constructor(request, session, client) {
|
|
14
|
-
super(request, session);
|
|
15
|
-
this.#session = session;
|
|
12
|
+
super(session, '#user', request, session);
|
|
16
13
|
this.#client = client;
|
|
17
|
-
// Trigger this
|
|
18
|
-
this.#dict;
|
|
19
14
|
}
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
return this
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
[ Symbol.iterator ]() { return this.entries()[ Symbol.iterator ](); }
|
|
26
|
-
|
|
27
|
-
get size() { return Object.keys(this.#dict).length; }
|
|
28
|
-
|
|
29
|
-
get(key) {
|
|
30
|
-
return Reflect.get(this.#dict, key);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
has(key) {
|
|
34
|
-
return Reflect.has(this.#dict, key);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
keys() {
|
|
38
|
-
return Object.keys(this.#dict);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
values() {
|
|
42
|
-
return Object.values(this.#dict);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
entries() {
|
|
46
|
-
return Object.entries(this.#dict);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
forEach(callback) {
|
|
50
|
-
this.entries().forEach(callback);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async set(key, value) {
|
|
54
|
-
if (!this.#session.has('user')) {
|
|
55
|
-
await this.#session.set('user', {});
|
|
56
|
-
}
|
|
57
|
-
Reflect.set(this.#dict, key, value);
|
|
58
|
-
await this.emit(key, value);
|
|
59
|
-
return this;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async delete(key) {
|
|
63
|
-
if (!this.#session.has('user')) {
|
|
64
|
-
await this.#session.set('user', {});
|
|
65
|
-
}
|
|
66
|
-
Reflect.deleteProperty(this.#dict, key);
|
|
67
|
-
await this.emit(key);
|
|
68
|
-
return this;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async clear() {
|
|
72
|
-
for (const key of this.keys()) {
|
|
73
|
-
Reflect.deleteProperty(this.#dict, key);
|
|
74
|
-
}
|
|
75
|
-
await this.emit();
|
|
76
|
-
return this;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async json(arg = null) {
|
|
80
|
-
if (!arguments.length || typeof arg === 'boolean') {
|
|
81
|
-
return {...this.#dict};
|
|
82
|
-
}
|
|
83
|
-
if (!_isObject(arg)) {
|
|
84
|
-
throw new Error(`Argument must be a valid JSON object`);
|
|
85
|
-
}
|
|
86
|
-
return await Promise.all(Object.entries(arg).map(([key, value]) => {
|
|
87
|
-
return this.set(key, value);
|
|
88
|
-
}));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
isSignedIn() {
|
|
92
|
-
return this.has('id');
|
|
16
|
+
async isSignedIn() {
|
|
17
|
+
return await this.has('id');
|
|
93
18
|
}
|
|
94
19
|
|
|
95
20
|
async signIn(...args) {
|
|
@@ -3,10 +3,14 @@ import { renderCookieObj } from './util-http.js';
|
|
|
3
3
|
import { WebfloStorage } from './WebfloStorage.js';
|
|
4
4
|
|
|
5
5
|
export class WebfloCookieStorage extends WebfloStorage {
|
|
6
|
+
|
|
7
|
+
#originals;
|
|
8
|
+
|
|
6
9
|
constructor(request, iterable = []) {
|
|
7
10
|
iterable = [...iterable].map(([key, value]) => [key, !_isObject(value) ? { name: key, value } : value]);
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
iterable = Object.fromEntries(iterable);
|
|
12
|
+
super(iterable, null, request);
|
|
13
|
+
this.#originals = { ...iterable };
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
async set(key, value) {
|
|
@@ -14,14 +18,26 @@ export class WebfloCookieStorage extends WebfloStorage {
|
|
|
14
18
|
return await super.set(key, value);
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
get(key, withDetail = false) {
|
|
18
|
-
if (!withDetail) return super.get(key)?.value;
|
|
19
|
-
return super.get(key);
|
|
21
|
+
async get(key, withDetail = false) {
|
|
22
|
+
if (!withDetail) return (await super.get(key))?.value;
|
|
23
|
+
return await super.get(key);
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
render() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
async render() {
|
|
27
|
+
const entries = await Promise.all((await this.keys()).concat(Object.keys(this.#originals)).map(async (key) => {
|
|
28
|
+
const a = Reflect.get(this.#originals, key);
|
|
29
|
+
const b = await this.get(key, true);
|
|
30
|
+
if (a === b || (_isObject(a) && _isObject(b) && _even(a, b))) {
|
|
31
|
+
// Same
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if ([undefined, null].includes(b)) {
|
|
35
|
+
// Deleted
|
|
36
|
+
return { name: key, value: '', maxAge: 0 };
|
|
37
|
+
}
|
|
38
|
+
// Added or modified
|
|
39
|
+
return { name: key, ...(await this.get(key, true)) };
|
|
40
|
+
})).then((entries) => entries.filter((e) => e));
|
|
41
|
+
return entries.map((e) => renderCookieObj(e));
|
|
26
42
|
}
|
|
27
43
|
}
|
|
@@ -37,8 +37,8 @@ export class WebfloRuntime {
|
|
|
37
37
|
? new Response(null, { status: 404 })
|
|
38
38
|
: Response.create(response);
|
|
39
39
|
}
|
|
40
|
-
// Commit data
|
|
41
|
-
for (const storage of [httpEvent.
|
|
40
|
+
// Commit data in the exact order. Reason: in how they depend on each other
|
|
41
|
+
for (const storage of [httpEvent.user, httpEvent.session, httpEvent.cookies]) {
|
|
42
42
|
await storage?.commit?.(response);
|
|
43
43
|
}
|
|
44
44
|
return response;
|
|
@@ -1,39 +1,83 @@
|
|
|
1
1
|
import { _isObject } from '@webqit/util/js/index.js';
|
|
2
2
|
import { _even } from '@webqit/util/obj/index.js';
|
|
3
3
|
|
|
4
|
-
export class WebfloStorage
|
|
4
|
+
export class WebfloStorage {
|
|
5
5
|
|
|
6
6
|
#request;
|
|
7
7
|
#session;
|
|
8
|
+
#registry;
|
|
9
|
+
#key;
|
|
10
|
+
#store;
|
|
8
11
|
|
|
9
|
-
constructor(request, session
|
|
10
|
-
|
|
12
|
+
constructor(registry, key, request, session = null) {
|
|
13
|
+
this.#registry = registry;
|
|
14
|
+
this.#key = key;
|
|
11
15
|
this.#request = request;
|
|
12
16
|
this.#session = session === true ? this : session;
|
|
13
|
-
for (const [k, v] of iterable) {
|
|
14
|
-
this.set(k, v);
|
|
15
|
-
}
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!this.#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
19
|
+
async store() {
|
|
20
|
+
if (!this.#key) {
|
|
21
|
+
return this.#registry;
|
|
22
|
+
}
|
|
23
|
+
if (!this.#store && !(this.#store = await this.#registry.get(this.#key))) {
|
|
24
|
+
this.#store = {};
|
|
25
|
+
await this.#registry.set(this.#key, this.#store);
|
|
26
|
+
}
|
|
27
|
+
return this.#store;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
if (!this.#
|
|
30
|
-
|
|
31
|
-
return !this.#originals.has(k) || (this.has(k) && ((a, b) => _isObject(a) && _isObject(b) ? !_even(a, b) : a !== b)(this.get(k, true), this.#originals.get(k)));
|
|
32
|
-
});
|
|
30
|
+
async commit() {
|
|
31
|
+
if (!this.#store || !this.#key) return;
|
|
32
|
+
await this.#registry.set(this.#key, this.#store);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
get size() { return this.store().then((store) => Object.keys(store).length); }
|
|
36
|
+
|
|
37
|
+
[ Symbol.iterator ]() { return this.entries().then((entries) => entries[ Symbol.iterator ]()); }
|
|
38
|
+
|
|
39
|
+
async json(arg = null) {
|
|
40
|
+
if (!arguments.length || typeof arg === 'boolean') {
|
|
41
|
+
return { ...(await this.store()) };
|
|
42
|
+
}
|
|
43
|
+
if (!_isObject(arg)) {
|
|
44
|
+
throw new Error(`Argument must be a valid JSON object`);
|
|
45
|
+
}
|
|
46
|
+
return await Promise.all(Object.entries(arg).map(([key, value]) => {
|
|
47
|
+
return this.set(key, value);
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async get(key) { return Reflect.get(await this.store(), key); }
|
|
52
|
+
|
|
53
|
+
async has(key) { return Reflect.has(await this.store(), key); }
|
|
54
|
+
|
|
55
|
+
async keys() { return Object.keys(await this.store()); }
|
|
56
|
+
|
|
57
|
+
async values() { return Object.values(await this.store()); }
|
|
58
|
+
|
|
59
|
+
async entries() { return Object.entries(await this.store()); }
|
|
60
|
+
|
|
61
|
+
async forEach(callback) { (await this.entries()).forEach(callback); }
|
|
62
|
+
|
|
63
|
+
async set(key, value) {
|
|
64
|
+
Reflect.set(await this.store(), key, value);
|
|
65
|
+
await this.emit(key, value);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async delete(key) {
|
|
70
|
+
Reflect.deleteProperty(await this.store(), key);
|
|
71
|
+
await this.emit(key);
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async clear() {
|
|
76
|
+
for (const key of await this.keys()) {
|
|
77
|
+
Reflect.deleteProperty(await this.store(), key);
|
|
78
|
+
}
|
|
79
|
+
await this.emit();
|
|
80
|
+
return this;
|
|
37
81
|
}
|
|
38
82
|
|
|
39
83
|
#listeners = new Set;
|
|
@@ -58,24 +102,6 @@ export class WebfloStorage extends Map {
|
|
|
58
102
|
return Promise.all(returnValues);
|
|
59
103
|
}
|
|
60
104
|
|
|
61
|
-
async set(attr, value) {
|
|
62
|
-
const returnValue = super.set(attr, value);
|
|
63
|
-
await this.emit(attr, value);
|
|
64
|
-
return returnValue;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async delete(attr) {
|
|
68
|
-
const returnValue = super.delete(attr);
|
|
69
|
-
await this.emit(attr);
|
|
70
|
-
return returnValue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async clear() {
|
|
74
|
-
const returnValue = super.clear();
|
|
75
|
-
await this.emit();
|
|
76
|
-
return returnValue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
105
|
#handlers = new Map;
|
|
80
106
|
defineHandler(attr, ...handlers) {
|
|
81
107
|
const $handlers = [];
|
|
@@ -97,7 +123,7 @@ export class WebfloStorage extends Map {
|
|
|
97
123
|
async require(attrs, callback = null, noNulls = false) {
|
|
98
124
|
const entries = [];
|
|
99
125
|
main: for await (const attr of [].concat(attrs)) {
|
|
100
|
-
if (!this.has(attr) || (noNulls && [undefined, null].includes(this.get(attr)))) {
|
|
126
|
+
if (!(await this.has(attr)) || (noNulls && [undefined, null].includes(await this.get(attr)))) {
|
|
101
127
|
const handlers = this.#handlers.get(attr);
|
|
102
128
|
if (!handlers) {
|
|
103
129
|
throw new Error(`No handler defined for the user attribute: ${attr}`);
|
|
@@ -132,7 +158,7 @@ export class WebfloStorage extends Map {
|
|
|
132
158
|
}});
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
|
-
entries.push(this.get(attr));
|
|
161
|
+
entries.push(await this.get(attr));
|
|
136
162
|
}
|
|
137
163
|
if (callback) return await callback(...entries);
|
|
138
164
|
return entries;
|
|
@@ -12,8 +12,8 @@ export class Capabilities {
|
|
|
12
12
|
static async initialize(params) {
|
|
13
13
|
const instance = new this;
|
|
14
14
|
instance.#params = params;
|
|
15
|
-
instance.#params.
|
|
16
|
-
instance.#params.
|
|
15
|
+
instance.#params.generic_public_webhook_url = instance.#params.generic_public_webhook_url_variable && instance.#params.env[instance.#params.generic_public_webhook_url_variable];
|
|
16
|
+
instance.#params.vapid_public_key = instance.#params.vapid_public_key_variable && instance.#params.env[instance.#params.vapid_public_key_variable];
|
|
17
17
|
// --------
|
|
18
18
|
// Custom install
|
|
19
19
|
const onbeforeinstallprompt = (e) => {
|
|
@@ -26,11 +26,11 @@ export class Capabilities {
|
|
|
26
26
|
instance.#cleanups.push(() => window.removeEventListener('beforeinstallprompt', onbeforeinstallprompt));
|
|
27
27
|
// --------
|
|
28
28
|
// Webhooks
|
|
29
|
-
if (instance.#params.
|
|
29
|
+
if (instance.#params.generic_public_webhook_url) {
|
|
30
30
|
// --------
|
|
31
31
|
// app.installed
|
|
32
32
|
const onappinstalled = () => {
|
|
33
|
-
fetch(instance.#params.
|
|
33
|
+
fetch(instance.#params.generic_public_webhook_url, {
|
|
34
34
|
method: 'POST',
|
|
35
35
|
headers: { 'Content-Type': 'application/json' },
|
|
36
36
|
body: JSON.stringify({ type: 'app.installed', data: true })
|
|
@@ -51,7 +51,7 @@ export class Capabilities {
|
|
|
51
51
|
if (eventPayload.type === 'push.subscribe' && !eventPayload.data) {
|
|
52
52
|
return window.queueMicrotask(pushPermissionStatusHandler);
|
|
53
53
|
}
|
|
54
|
-
fetch(instance.#params.
|
|
54
|
+
fetch(instance.#params.generic_public_webhook_url, {
|
|
55
55
|
method: 'POST',
|
|
56
56
|
headers: { 'Content-Type': 'application/json' },
|
|
57
57
|
body: JSON.stringify(eventPayload)
|
|
@@ -184,8 +184,8 @@ export class Capabilities {
|
|
|
184
184
|
if (!params.userVisibleOnly) {
|
|
185
185
|
params = { ...params, userVisibleOnly: true };
|
|
186
186
|
}
|
|
187
|
-
if (!params.applicationServerKey && this.#params.
|
|
188
|
-
params = { ...params, applicationServerKey: urlBase64ToUint8Array(this.#params.
|
|
187
|
+
if (!params.applicationServerKey && this.#params.vapid_public_key) {
|
|
188
|
+
params = { ...params, applicationServerKey: urlBase64ToUint8Array(this.#params.vapid_public_key) };
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
return params;
|
|
@@ -1,33 +1,16 @@
|
|
|
1
1
|
import { WebfloStorage } from '../WebfloStorage.js';
|
|
2
2
|
|
|
3
3
|
export class SessionStorage extends WebfloStorage {
|
|
4
|
-
static get type() { return 'session'; }
|
|
5
|
-
|
|
6
4
|
static create(request) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
const registry = {
|
|
6
|
+
async get(key) { return localStorage.getItem(key) },
|
|
7
|
+
async set(key, value) { return localStorage.setItem(key, value) },
|
|
8
|
+
};
|
|
9
|
+
return new this(
|
|
10
|
+
registry,
|
|
11
|
+
'session',
|
|
13
12
|
request,
|
|
14
|
-
|
|
13
|
+
true
|
|
15
14
|
);
|
|
16
|
-
return instance;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
constructor(request, iterable) {
|
|
20
|
-
super(request, true, iterable);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async commit() {
|
|
24
|
-
const storeType = this.constructor.type === 'user' ? 'localStorage' : 'sessionStorage';
|
|
25
|
-
for (const key of this.getAdded()) {
|
|
26
|
-
window[storeType].setItem(key, this.get(key));
|
|
27
|
-
}
|
|
28
|
-
for (const key of this.getDeleted()) {
|
|
29
|
-
window[storeType].removeItem(key);
|
|
30
|
-
}
|
|
31
|
-
await super.commit();
|
|
32
15
|
}
|
|
33
16
|
}
|
|
@@ -8,9 +8,11 @@ export class CookieStorage extends WebfloCookieStorage {
|
|
|
8
8
|
);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
async commit(response) {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
async commit(response = null) {
|
|
12
|
+
if (response) {
|
|
13
|
+
for (const cookieStr of await this.render()) {
|
|
14
|
+
response.headers.append('Set-Cookie', cookieStr);
|
|
15
|
+
}
|
|
14
16
|
}
|
|
15
17
|
await super.commit();
|
|
16
18
|
}
|
|
@@ -2,14 +2,10 @@ import { WebfloStorage } from '../../WebfloStorage.js';
|
|
|
2
2
|
|
|
3
3
|
export class SessionStorage extends WebfloStorage {
|
|
4
4
|
static create(request) {
|
|
5
|
-
return new this(request);
|
|
5
|
+
return new this({}, null, request);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
super(request, true);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async commit(response) {
|
|
8
|
+
async commit(response = null) {
|
|
13
9
|
await super.commit();
|
|
14
10
|
}
|
|
15
11
|
}
|
|
@@ -184,7 +184,7 @@ export class WebfloWorker extends WebfloRuntime {
|
|
|
184
184
|
// Restore session before dispatching
|
|
185
185
|
if (scope.request.method === 'GET'
|
|
186
186
|
&& (scope.redirectMessageID = scope.httpEvent.url.query['redirect-message'])
|
|
187
|
-
&& (scope.redirectMessage = scope.session.get(`redirect-message:${scope.redirectMessageID}`))) {
|
|
187
|
+
&& (scope.redirectMessage = await scope.session.get(`redirect-message:${scope.redirectMessageID}`))) {
|
|
188
188
|
await scope.session.delete(`redirect-message:${scope.redirectMessageID}`);
|
|
189
189
|
}
|
|
190
190
|
// Dispatch for response
|
|
@@ -8,9 +8,11 @@ export class CookieStorage extends WebfloCookieStorage {
|
|
|
8
8
|
);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
async commit(response) {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
async commit(response = null) {
|
|
12
|
+
if (response) {
|
|
13
|
+
for (const cookieStr of await this.render()) {
|
|
14
|
+
response.headers.append('Set-Cookie', cookieStr);
|
|
15
|
+
}
|
|
14
16
|
}
|
|
15
17
|
await super.commit();
|
|
16
18
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WebfloStorage } from '../WebfloStorage.js';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const inmemSessionRegistry = new Map;
|
|
5
5
|
export class SessionStorage extends WebfloStorage {
|
|
6
6
|
|
|
7
7
|
static create(request, params = {}) {
|
|
@@ -26,28 +26,26 @@ export class SessionStorage extends WebfloStorage {
|
|
|
26
26
|
sessionID = crypto.randomUUID();
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
|
|
30
|
-
return sessionStorage.get(sessionID);
|
|
31
|
-
}
|
|
32
|
-
const instance = new this(request, sessionID);
|
|
33
|
-
sessionStorage.set(sessionID, instance);
|
|
34
|
-
return instance;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
constructor(request, sessionID) {
|
|
38
|
-
super(request, true);
|
|
39
|
-
this.#sessionID = sessionID;
|
|
29
|
+
return new this(params.registry || inmemSessionRegistry, sessionID, request);
|
|
40
30
|
}
|
|
41
31
|
|
|
42
32
|
#sessionID;
|
|
43
|
-
get sessionID() {
|
|
44
|
-
|
|
33
|
+
get sessionID() { return this.#sessionID; }
|
|
34
|
+
|
|
35
|
+
constructor(reqistry, sessionID, request) {
|
|
36
|
+
super(
|
|
37
|
+
reqistry,
|
|
38
|
+
`session:${sessionID}`,
|
|
39
|
+
request,
|
|
40
|
+
true
|
|
41
|
+
);
|
|
42
|
+
this.#sessionID = sessionID;
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
async commit(response
|
|
48
|
-
if (response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid'))
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
async commit(response = null) {
|
|
46
|
+
if (response && !response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid')) {
|
|
47
|
+
response.headers.append('Set-Cookie', `__sessid=${this.#sessionID}; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=31536000`);
|
|
48
|
+
}
|
|
51
49
|
await super.commit();
|
|
52
50
|
}
|
|
53
51
|
}
|
|
@@ -4,7 +4,6 @@ import Path from 'path';
|
|
|
4
4
|
import Http from 'http';
|
|
5
5
|
import Https from 'https';
|
|
6
6
|
import WebSocket from 'ws';
|
|
7
|
-
import webpush from 'web-push';
|
|
8
7
|
import Mime from 'mime-types';
|
|
9
8
|
import QueryString from 'querystring';
|
|
10
9
|
import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
|
|
@@ -50,11 +49,14 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
50
49
|
#cx;
|
|
51
50
|
#servers = new Map;
|
|
52
51
|
#proxies = new Map;
|
|
53
|
-
#
|
|
52
|
+
#capabilitiesPromise;
|
|
54
53
|
|
|
55
54
|
// Typically for access by Router
|
|
56
55
|
get cx() { return this.#cx; }
|
|
57
56
|
|
|
57
|
+
#sdk = {};
|
|
58
|
+
get sdk() { return this.#sdk; }
|
|
59
|
+
|
|
58
60
|
constructor(cx) {
|
|
59
61
|
super();
|
|
60
62
|
if (!(cx instanceof this.constructor.Context)) {
|
|
@@ -100,6 +102,53 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
100
102
|
}));
|
|
101
103
|
}
|
|
102
104
|
// ---------------
|
|
105
|
+
const setupCapabilities = async () => {
|
|
106
|
+
if (this.#cx.server.capabilities?.database) {
|
|
107
|
+
if (this.#cx.server.capabilities.database_dialect !== 'postgres') {
|
|
108
|
+
throw new Error(`Only postgres supported for now for database dialect`);
|
|
109
|
+
}
|
|
110
|
+
if (this.#cx.env.entries[this.#cx.server.capabilities.database_url_variable]) {
|
|
111
|
+
console.log('Database capabilities');
|
|
112
|
+
const { SQLClient } = await import('@linked-db/linked-ql/sql');
|
|
113
|
+
const { default: pg } = await import('pg');
|
|
114
|
+
// Obtain pg client
|
|
115
|
+
const pgClient = new pg.Pool({
|
|
116
|
+
connectionString: this.#cx.env.entries[this.#cx.server.capabilities.database_url_variable],
|
|
117
|
+
database: 'postgres',
|
|
118
|
+
});
|
|
119
|
+
// Connect
|
|
120
|
+
await pgClient.connect();
|
|
121
|
+
this.#sdk.db = new SQLClient(pgClient, { dialect: 'postgres' });
|
|
122
|
+
} else {
|
|
123
|
+
console.log('No database capabilities');
|
|
124
|
+
//const { ODBClient } = await import('@linked-db/linked-ql/odb');
|
|
125
|
+
//this.#sdk.db = new ODBClient({ dialect: 'postgres' });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (this.#cx.server.capabilities?.redis && this.#cx.env.entries[this.#cx.server.capabilities.redis_url_variable]) {
|
|
129
|
+
const { Redis } = await import('ioredis');
|
|
130
|
+
this.#sdk.redis = !this.#cx.env.entries[this.#cx.server.capabilities.redis_url_variable]
|
|
131
|
+
? new Redis : new Redis(this.#cx.env.entries[this.#cx.server.capabilities.redis_url_variable], {
|
|
132
|
+
tls: { rejectUnauthorized: false }, // Required for Upstash
|
|
133
|
+
});
|
|
134
|
+
console.log('Redis capabilities');
|
|
135
|
+
}
|
|
136
|
+
if (this.#cx.server.capabilities?.webpush) {
|
|
137
|
+
const { default: webpush } = await import('web-push');
|
|
138
|
+
console.log('Webpuah capabilities');
|
|
139
|
+
this.#sdk.webpush = webpush;
|
|
140
|
+
if (this.#cx.env.entries[this.#cx.server.capabilities.vapid_public_key_variable]
|
|
141
|
+
&& this.#cx.env.entries[this.#cx.server.capabilities.vapid_private_key_variable]) {
|
|
142
|
+
webpush.setVapidDetails(
|
|
143
|
+
this.#cx.server.capabilities.vapid_subject,
|
|
144
|
+
this.#cx.env.entries[this.#cx.server.capabilities.vapid_public_key_variable],
|
|
145
|
+
this.#cx.env.entries[this.#cx.server.capabilities.vapid_private_key_variable]
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
this.#capabilitiesPromise = setupCapabilities();
|
|
151
|
+
// ---------------
|
|
103
152
|
this.control();
|
|
104
153
|
if (this.#cx.logger) {
|
|
105
154
|
if (this.#servers.size) {
|
|
@@ -179,6 +228,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
179
228
|
|
|
180
229
|
#globalMessagingRegistry = new Map;
|
|
181
230
|
async handleNodeWsRequest(wss, nodeRequest, socket, head) {
|
|
231
|
+
await this.#capabilitiesPromise;
|
|
182
232
|
const proto = this.getRequestProto(nodeRequest).replace('http', 'ws');
|
|
183
233
|
const [fullUrl, requestInit] = this.parseNodeRequest(proto, nodeRequest, false);
|
|
184
234
|
const scope = {};
|
|
@@ -239,6 +289,7 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
239
289
|
}
|
|
240
290
|
|
|
241
291
|
async handleNodeHttpRequest(nodeRequest, nodeResponse) {
|
|
292
|
+
await this.#capabilitiesPromise;
|
|
242
293
|
const proto = this.getRequestProto(nodeRequest);
|
|
243
294
|
const [fullUrl, requestInit] = this.parseNodeRequest(proto, nodeRequest);
|
|
244
295
|
const scope = {};
|
|
@@ -507,7 +558,13 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
507
558
|
: [];
|
|
508
559
|
scope.request = this.createRequest(scope.url.href, scope.init, scope.autoHeaders.filter((header) => header.type === 'request'));
|
|
509
560
|
scope.cookies = this.constructor.CookieStorage.create(scope.request);
|
|
510
|
-
scope.session = this.constructor.SessionStorage.create(scope.request, {
|
|
561
|
+
scope.session = this.constructor.SessionStorage.create(scope.request, {
|
|
562
|
+
secret: this.#cx.env.entries[this.#cx.server.session_key_variable],
|
|
563
|
+
registry: this.#sdk.redis && {
|
|
564
|
+
get: async (key) => { return await this.#sdk.redis.hgetall(key) },
|
|
565
|
+
set: async (key, value) => { return await this.#sdk.redis.hset(key, value) },
|
|
566
|
+
},
|
|
567
|
+
});
|
|
511
568
|
const sessionID = scope.session.sessionID;
|
|
512
569
|
if (!this.#globalMessagingRegistry.has(sessionID)) {
|
|
513
570
|
this.#globalMessagingRegistry.set(sessionID, new ClientMessagingRegistry(this, sessionID));
|
|
@@ -526,22 +583,13 @@ export class WebfloServer extends WebfloRuntime {
|
|
|
526
583
|
session: scope.session,
|
|
527
584
|
user: scope.user,
|
|
528
585
|
client: scope.clientMessaging,
|
|
529
|
-
sdk:
|
|
586
|
+
sdk: this.#sdk
|
|
530
587
|
});
|
|
531
|
-
if (this.#cx.server.capabilities?.webpush
|
|
532
|
-
&& this.#cx.env.entries[this.#cx.server.capabilities.app_vapid_public_key_variable]
|
|
533
|
-
&& this.#cx.env.entries[this.#cx.server.capabilities.app_vapid_private_key_variable]) {
|
|
534
|
-
webpush.setVapidDetails(
|
|
535
|
-
scope.url.origin.replace(/^http:/i, 'https:'),
|
|
536
|
-
this.#cx.env.entries[this.#cx.server.capabilities.app_vapid_public_key_variable],
|
|
537
|
-
this.#cx.env.entries[this.#cx.server.capabilities.app_vapid_private_key_variable]
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
588
|
await this.setup(scope.httpEvent);
|
|
541
589
|
// Restore session before dispatching
|
|
542
590
|
if (scope.request.method === 'GET'
|
|
543
591
|
&& (scope.redirectMessageID = scope.httpEvent.url.query['redirect-message'])
|
|
544
|
-
&& (scope.redirectMessage = scope.session.get(`redirect-message:${scope.redirectMessageID}`))) {
|
|
592
|
+
&& (scope.redirectMessage = await scope.session.get(`redirect-message:${scope.redirectMessageID}`))) {
|
|
545
593
|
await scope.session.delete(`redirect-message:${scope.redirectMessageID}`);
|
|
546
594
|
}
|
|
547
595
|
// Dispatch for response
|