@omerlo/omerlo-webkit 0.0.4 → 0.0.5
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 +24 -22
- package/dist/components/OmerloWebkit.svelte +2 -3
- package/dist/components/OmerloWebkit.svelte.d.ts +1 -1
- package/dist/omerlo/index.d.ts +4 -2
- package/dist/omerlo/reader/endpoints/categories.d.ts +18 -0
- package/dist/omerlo/reader/endpoints/categories.js +36 -0
- package/dist/omerlo/reader/endpoints/content-templates.d.ts +16 -0
- package/dist/omerlo/reader/endpoints/content-templates.js +1 -0
- package/dist/omerlo/reader/endpoints/contents.d.ts +51 -0
- package/dist/omerlo/reader/endpoints/contents.js +2 -0
- package/dist/omerlo/reader/endpoints/device.js +1 -1
- package/dist/omerlo/reader/endpoints/notification.d.ts +5 -5
- package/dist/omerlo/reader/endpoints/notification.js +5 -7
- package/dist/omerlo/reader/endpoints/oauth.js +1 -1
- package/dist/omerlo/reader/endpoints/visuals.d.ts +16 -0
- package/dist/omerlo/reader/endpoints/visuals.js +1 -0
- package/dist/omerlo/reader/fetchers.d.ts +4 -2
- package/dist/omerlo/reader/fetchers.js +3 -1
- package/dist/omerlo/reader/server/email.d.ts +12 -0
- package/dist/omerlo/reader/server/email.js +30 -0
- package/dist/omerlo/reader/server/hooks.d.ts +1 -1
- package/dist/omerlo/reader/server/hooks.js +25 -20
- package/dist/omerlo/reader/server/index.d.ts +1 -0
- package/dist/omerlo/reader/server/index.js +1 -0
- package/dist/omerlo/reader/server/token.d.ts +6 -6
- package/dist/omerlo/reader/server/token.js +7 -7
- package/dist/omerlo/reader/server/utils.d.ts +4 -4
- package/dist/omerlo/reader/server/utils.js +13 -7
- package/dist/omerlo/reader/stores/user_session.d.ts +2 -2
- package/dist/omerlo/reader/stores/user_session.js +11 -9
- package/dist/omerlo/reader/utils/request.d.ts +10 -8
- package/dist/omerlo/reader/utils/request.js +39 -11
- package/dist/omerlo/reader/utils/response.d.ts +1 -1
- package/dist/omerlo/reader/utils/response.js +1 -1
- package/eslint.config.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,17 +24,17 @@ Copy the `.env.dist` to `.env` then update its values to match your Omerlo's app
|
|
|
24
24
|
Create (or update) the file `src/hooks.server.ts` and add required hooks as follow
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
|
-
import type { Handle } from
|
|
27
|
+
import type { Handle } from '@sveltejs/kit';
|
|
28
28
|
|
|
29
29
|
import { handleUserToken, handleReaderApi } from 'omerlo-webkit/reader/server';
|
|
30
|
-
import { sequence } from
|
|
30
|
+
import { sequence } from '@sveltejs/kit/hooks';
|
|
31
31
|
|
|
32
32
|
// You can add custom hooks too
|
|
33
33
|
const handleLocale: Handle = async ({ event, resolve }) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
34
|
+
const userLocale = 'en'; // you can fetch this value from cookie or w.e
|
|
35
|
+
event.url.searchParams.append('locale', userLocale);
|
|
36
|
+
return resolve(event);
|
|
37
|
+
};
|
|
38
38
|
|
|
39
39
|
export const handle = sequence(handleLocale, handleUserToken, handleReaderApi);
|
|
40
40
|
```
|
|
@@ -92,7 +92,7 @@ You'll need to create a login endpoint to add some required params such as the `
|
|
|
92
92
|
|
|
93
93
|
```ts
|
|
94
94
|
// +server.ts
|
|
95
|
-
import { error, redirect, type RequestHandler } from
|
|
95
|
+
import { error, redirect, type RequestHandler } from '@sveltejs/kit';
|
|
96
96
|
import { env } from '$env/dynamic/private';
|
|
97
97
|
import jwt from 'jsonwebtoken';
|
|
98
98
|
|
|
@@ -104,39 +104,41 @@ export const GET: RequestHandler = ({ url }) => {
|
|
|
104
104
|
if (!oauthProviderId) error(400, 'Missing oauthProviderId query parameter');
|
|
105
105
|
|
|
106
106
|
const currentPath = url.searchParams.get('currentPath') || '/';
|
|
107
|
-
const state = jwt.sign({ currentPath, oauthProviderId }, env.PRIVATE_JWT_SECRET, {
|
|
107
|
+
const state = jwt.sign({ currentPath, oauthProviderId }, env.PRIVATE_JWT_SECRET, {
|
|
108
|
+
expiresIn: '1h'
|
|
109
|
+
});
|
|
108
110
|
|
|
109
111
|
const redirectUrl = new URL(oauthUrl);
|
|
110
112
|
redirectUrl.searchParams.set('state', state);
|
|
111
|
-
redirectUrl.searchParams.set('redirect_uri', url.origin +'/oauth/callback');
|
|
113
|
+
redirectUrl.searchParams.set('redirect_uri', url.origin + '/oauth/callback');
|
|
112
114
|
|
|
113
115
|
redirect(302, redirectUrl);
|
|
114
|
-
}
|
|
116
|
+
};
|
|
115
117
|
```
|
|
116
118
|
|
|
117
119
|
And then create a callback action.
|
|
118
120
|
|
|
119
121
|
```ts
|
|
120
|
-
import { error, redirect, type RequestHandler } from
|
|
122
|
+
import { error, redirect, type RequestHandler } from '@sveltejs/kit';
|
|
121
123
|
import { env } from '$env/dynamic/private';
|
|
122
124
|
import jwt from 'jsonwebtoken';
|
|
123
|
-
import { exchangeAuthorizationCode, setAuthorizationCookies } from
|
|
125
|
+
import { exchangeAuthorizationCode, setAuthorizationCookies } from 'omerlo-webkit/reader/server';
|
|
124
126
|
|
|
125
127
|
export const GET: RequestHandler = async ({ url, cookies }) => {
|
|
126
128
|
const redirectUri = url.origin + url.pathname;
|
|
127
129
|
const state = getRequiredQueryParams(url, 'state');
|
|
128
130
|
const code = getRequiredQueryParams(url, 'code');
|
|
129
|
-
const {oauthProviderId, currentPath} = parseJwt(state);
|
|
131
|
+
const { oauthProviderId, currentPath } = parseJwt(state);
|
|
130
132
|
|
|
131
133
|
try {
|
|
132
|
-
const token = await exchangeAuthorizationCode({code, redirectUri, oauthProviderId})
|
|
134
|
+
const token = await exchangeAuthorizationCode({ code, redirectUri, oauthProviderId });
|
|
133
135
|
setAuthorizationCookies(cookies, token);
|
|
134
|
-
} catch(_err) {
|
|
135
|
-
error(401,
|
|
136
|
+
} catch (_err) {
|
|
137
|
+
error(401, 'Could not authenticate from the provider');
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
redirect(303, currentPath);
|
|
139
|
-
}
|
|
141
|
+
};
|
|
140
142
|
|
|
141
143
|
function getRequiredQueryParams(url: URL, paramsName: string): string {
|
|
142
144
|
const value = url.searchParams.get(paramsName);
|
|
@@ -149,14 +151,14 @@ function getRequiredQueryParams(url: URL, paramsName: string): string {
|
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
interface State {
|
|
152
|
-
oauthProviderId: string
|
|
153
|
-
currentPath: string
|
|
154
|
+
oauthProviderId: string;
|
|
155
|
+
currentPath: string;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
function parseJwt(state: string): State {
|
|
157
159
|
try {
|
|
158
160
|
return jwt.verify(state, env.PRIVATE_JWT_SECRET) as State;
|
|
159
|
-
} catch(_err) {
|
|
161
|
+
} catch (_err) {
|
|
160
162
|
error(400, 'Invalid state');
|
|
161
163
|
}
|
|
162
164
|
}
|
|
@@ -168,13 +170,13 @@ To logout your user you can create an endpoint that will drop cookies used for a
|
|
|
168
170
|
|
|
169
171
|
```ts
|
|
170
172
|
// +server.ts
|
|
171
|
-
import { json, type RequestHandler } from
|
|
173
|
+
import { json, type RequestHandler } from '@sveltejs/kit';
|
|
172
174
|
import { clearAuthorizationCookies } from 'omerlo-webkit/reader/server';
|
|
173
175
|
|
|
174
176
|
export const DELETE: RequestHandler = ({ cookies }) => {
|
|
175
177
|
clearAuthorizationCookies(cookies);
|
|
176
178
|
return json(201);
|
|
177
|
-
}
|
|
179
|
+
};
|
|
178
180
|
```
|
|
179
181
|
|
|
180
182
|
When a user is logout for any reason (401 or manually logout), the `invalidate('omerlo:user_session)` is triggered,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { initUserSession, type UserSession } from
|
|
3
|
-
import { onDestroy, onMount } from
|
|
2
|
+
import { initUserSession, type UserSession } from '../omerlo/reader/stores/user_session';
|
|
3
|
+
import { onDestroy, onMount } from 'svelte';
|
|
4
4
|
|
|
5
5
|
export let userSession: UserSession;
|
|
6
6
|
let selfComponent: HTMLDivElement;
|
|
@@ -23,4 +23,3 @@
|
|
|
23
23
|
<div id="omerlo-webkit" bind:this={selfComponent}>
|
|
24
24
|
<slot></slot>
|
|
25
25
|
</div>
|
|
26
|
-
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type UserSession } from
|
|
1
|
+
import { type UserSession } from '../omerlo/reader/stores/user_session';
|
|
2
2
|
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
3
|
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
4
|
$$bindings?: Bindings;
|
package/dist/omerlo/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export declare const useReader: (f: typeof fetch) => {
|
|
2
2
|
notifications: {
|
|
3
3
|
listTopics: (params?: Partial<import("./reader/utils/api").PagingParams>) => Promise<import("./reader/utils/api").ApiResponse<import("./reader/endpoints/notification").TopicSummary[]>>;
|
|
4
|
-
subscribeToTopic: (params: import("./reader/endpoints/notification").
|
|
5
|
-
unsubscribeFromTopic: (params: import("./reader/endpoints/notification").
|
|
4
|
+
subscribeToTopic: (params: import("./reader/endpoints/notification").SubscriptionParams) => Promise<import("./reader/utils/api").ApiResponse<unknown>>;
|
|
5
|
+
unsubscribeFromTopic: (params: import("./reader/endpoints/notification").SubscriptionParams) => Promise<import("./reader/utils/api").ApiResponse<unknown>>;
|
|
6
6
|
};
|
|
7
|
+
listCategories: (params?: Partial<import("./reader/utils/api").PagingParams>) => Promise<import("./reader/utils/api").ApiResponse<import("./reader/endpoints/categories").Category[]>>;
|
|
8
|
+
getCategory: (id: string) => Promise<import("./reader/utils/api").ApiResponse<import("./reader/endpoints/categories").Category>>;
|
|
7
9
|
listOauthProviders: (params?: Partial<import("./reader/utils/api").PagingParams>) => Promise<import("./reader/utils/api").ApiResponse<import("./reader").OauthProviderSummary[]>>;
|
|
8
10
|
getOauthUser: () => Promise<import("./reader/utils/api").ApiResponse<import("./reader").OauthUser>>;
|
|
9
11
|
registerDevice: (params: import("./reader/endpoints/device").DeviceParams) => Promise<import("./reader/utils/api").ApiResponse<unknown>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type LocalesMetadata } from '../utils/response';
|
|
2
|
+
import { type ApiAssocs, type ApiData, type PagingParams } from '../utils/api';
|
|
3
|
+
export declare const categoriesFetchers: (f: typeof fetch) => {
|
|
4
|
+
listCategories: (params?: Partial<PagingParams>) => Promise<import("../utils/api").ApiResponse<Category[]>>;
|
|
5
|
+
getCategory: (id: string) => Promise<import("../utils/api").ApiResponse<Category>>;
|
|
6
|
+
};
|
|
7
|
+
export interface Category {
|
|
8
|
+
id: string;
|
|
9
|
+
svg: string;
|
|
10
|
+
name: string;
|
|
11
|
+
meta: {
|
|
12
|
+
locale: LocalesMetadata;
|
|
13
|
+
};
|
|
14
|
+
updatedAt: Date;
|
|
15
|
+
}
|
|
16
|
+
export declare function getCategory(f: typeof fetch): (id: string) => Promise<import("../utils/api").ApiResponse<Category>>;
|
|
17
|
+
export declare function listCategories(f: typeof fetch): (params?: Partial<PagingParams>) => Promise<import("../utils/api").ApiResponse<Category[]>>;
|
|
18
|
+
export declare function parseCategory(data: ApiData, _assoc: ApiAssocs): Category;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {} from '../utils/response';
|
|
2
|
+
import { parseMany } from '../utils/api';
|
|
3
|
+
import { requestPublisher } from '../utils/request';
|
|
4
|
+
export const categoriesFetchers = (f) => {
|
|
5
|
+
return {
|
|
6
|
+
listCategories: listCategories(f),
|
|
7
|
+
getCategory: getCategory(f)
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export function getCategory(f) {
|
|
11
|
+
return async (id) => {
|
|
12
|
+
const opts = { parser: parseCategory };
|
|
13
|
+
return requestPublisher(f, `/categories/${id}`, opts);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function listCategories(f) {
|
|
17
|
+
return async (params) => {
|
|
18
|
+
const queryParams = params;
|
|
19
|
+
const opts = { parser: parseMany(parseCategory), queryParams };
|
|
20
|
+
return requestPublisher(f, `/categories`, opts);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function parseCategory(data, _assoc) {
|
|
24
|
+
return {
|
|
25
|
+
id: data.id,
|
|
26
|
+
svg: data.svg_icon,
|
|
27
|
+
name: data.localized.name,
|
|
28
|
+
updatedAt: data.updated_at,
|
|
29
|
+
meta: {
|
|
30
|
+
locale: {
|
|
31
|
+
available: [data.localized.locale],
|
|
32
|
+
current: data.localized.locale
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LocalesMetadata } from '../utils/response';
|
|
2
|
+
export interface ContentTemplate {
|
|
3
|
+
id: string;
|
|
4
|
+
key: string;
|
|
5
|
+
name: string;
|
|
6
|
+
meta: {
|
|
7
|
+
locales: LocalesMetadata;
|
|
8
|
+
};
|
|
9
|
+
metadata: Record<string, string>;
|
|
10
|
+
enableFields: string[];
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
export interface ContentBlockTemplate {
|
|
14
|
+
id: string;
|
|
15
|
+
key: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { LocalesMetadata } from '../utils/response';
|
|
2
|
+
import type { Category } from './categories';
|
|
3
|
+
import type { ContentBlockTemplate, ContentTemplate } from './content-templates';
|
|
4
|
+
import type { Visual } from './visuals';
|
|
5
|
+
export interface ContentSummary {
|
|
6
|
+
id: string;
|
|
7
|
+
template: ContentTemplate;
|
|
8
|
+
metadata: Record<string, string>;
|
|
9
|
+
canonicalDomain: string;
|
|
10
|
+
canonicalUrl: string;
|
|
11
|
+
isArchived: boolean;
|
|
12
|
+
publishedAt?: Date;
|
|
13
|
+
visibility?: string;
|
|
14
|
+
categories: Category[];
|
|
15
|
+
show_published_at: boolean;
|
|
16
|
+
updatedAt: Date;
|
|
17
|
+
meta: {
|
|
18
|
+
locales: LocalesMetadata;
|
|
19
|
+
};
|
|
20
|
+
titleHtml: string;
|
|
21
|
+
titleText: string;
|
|
22
|
+
leadHtml: string;
|
|
23
|
+
leadText: string;
|
|
24
|
+
subtitleHtml: string;
|
|
25
|
+
subtitleText: string;
|
|
26
|
+
visual: Visual;
|
|
27
|
+
seo: ContentSeo;
|
|
28
|
+
}
|
|
29
|
+
export interface ContentSeo {
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
}
|
|
33
|
+
export interface Content extends ContentSummary {
|
|
34
|
+
blocks: ContentBlock[];
|
|
35
|
+
}
|
|
36
|
+
export type ContentBlock = ContentBlockRichtext | ContentBlockData;
|
|
37
|
+
export type ContentBlockRichtext = {
|
|
38
|
+
id: string;
|
|
39
|
+
template: ContentBlockTemplate;
|
|
40
|
+
visual?: Visual;
|
|
41
|
+
kind: 'richtext';
|
|
42
|
+
contentHtml: string;
|
|
43
|
+
};
|
|
44
|
+
export type ContentBlockData = {
|
|
45
|
+
id: string;
|
|
46
|
+
template: ContentBlockTemplate;
|
|
47
|
+
visual?: Visual;
|
|
48
|
+
kind: 'data';
|
|
49
|
+
contentType: string;
|
|
50
|
+
data: unknown;
|
|
51
|
+
};
|
|
@@ -2,8 +2,8 @@ import { type ApiAssocs, type ApiData, type PagingParams } from '../utils/api';
|
|
|
2
2
|
import { type LocalesMetadata } from '../utils/response';
|
|
3
3
|
export declare const notificationFetchers: (f: typeof fetch) => {
|
|
4
4
|
listTopics: (params?: Partial<PagingParams>) => Promise<import("../utils/api").ApiResponse<TopicSummary[]>>;
|
|
5
|
-
subscribeToTopic: (params:
|
|
6
|
-
unsubscribeFromTopic: (params:
|
|
5
|
+
subscribeToTopic: (params: SubscriptionParams) => Promise<import("../utils/api").ApiResponse<unknown>>;
|
|
6
|
+
unsubscribeFromTopic: (params: SubscriptionParams) => Promise<import("../utils/api").ApiResponse<unknown>>;
|
|
7
7
|
};
|
|
8
8
|
export declare function listTopics(f: typeof fetch): (params?: Partial<PagingParams>) => Promise<import("../utils/api").ApiResponse<TopicSummary[]>>;
|
|
9
9
|
export declare function parseTopicSummary(data: ApiData, _assocs: ApiAssocs): TopicSummary;
|
|
@@ -15,9 +15,9 @@ export interface TopicSummary {
|
|
|
15
15
|
};
|
|
16
16
|
updatedAt: Date;
|
|
17
17
|
}
|
|
18
|
-
export interface
|
|
18
|
+
export interface SubscriptionParams {
|
|
19
19
|
topicId: string;
|
|
20
20
|
pushToken: string;
|
|
21
21
|
}
|
|
22
|
-
export declare function subscribeToTopic(f: typeof fetch): (params:
|
|
23
|
-
export declare function unsubscribeFromTopic(f: typeof fetch): (params:
|
|
22
|
+
export declare function subscribeToTopic(f: typeof fetch): (params: SubscriptionParams) => Promise<import("../utils/api").ApiResponse<unknown>>;
|
|
23
|
+
export declare function unsubscribeFromTopic(f: typeof fetch): (params: SubscriptionParams) => Promise<import("../utils/api").ApiResponse<unknown>>;
|
|
@@ -5,7 +5,7 @@ export const notificationFetchers = (f) => {
|
|
|
5
5
|
return {
|
|
6
6
|
listTopics: listTopics(f),
|
|
7
7
|
subscribeToTopic: subscribeToTopic(f),
|
|
8
|
-
unsubscribeFromTopic: unsubscribeFromTopic(f)
|
|
8
|
+
unsubscribeFromTopic: unsubscribeFromTopic(f)
|
|
9
9
|
};
|
|
10
10
|
};
|
|
11
11
|
export function listTopics(f) {
|
|
@@ -27,18 +27,16 @@ export function parseTopicSummary(data, _assocs) {
|
|
|
27
27
|
export function subscribeToTopic(f) {
|
|
28
28
|
return (params) => {
|
|
29
29
|
const body = { push_token: params.pushToken };
|
|
30
|
-
const headers = new Headers();
|
|
31
|
-
headers
|
|
32
|
-
const opts = { method: 'post', body, headers };
|
|
30
|
+
const headers = new Headers({ 'Content-Type': 'application/json' });
|
|
31
|
+
const opts = { body, headers, method: 'post' };
|
|
33
32
|
return request(f, `/topics/${params.topicId}/subscribe`, opts);
|
|
34
33
|
};
|
|
35
34
|
}
|
|
36
35
|
export function unsubscribeFromTopic(f) {
|
|
37
36
|
return (params) => {
|
|
38
37
|
const body = { push_token: params.pushToken };
|
|
39
|
-
const headers = new Headers();
|
|
40
|
-
headers
|
|
41
|
-
const opts = { method: 'post', body, headers };
|
|
38
|
+
const headers = new Headers({ 'Content-Type': 'application/json' });
|
|
39
|
+
const opts = { body, headers, method: 'post' };
|
|
42
40
|
return request(f, `/topics/${params.topicId}/unsubscribe`, opts);
|
|
43
41
|
};
|
|
44
42
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Visual = Image | Slideshow | Video;
|
|
2
|
+
export type Image = {
|
|
3
|
+
type: 'image';
|
|
4
|
+
url: string;
|
|
5
|
+
captionHtml: string;
|
|
6
|
+
captionText: string;
|
|
7
|
+
credit: string;
|
|
8
|
+
gravity: Gravity;
|
|
9
|
+
};
|
|
10
|
+
export type Slideshow = {
|
|
11
|
+
type: 'slideshow';
|
|
12
|
+
};
|
|
13
|
+
export type Video = {
|
|
14
|
+
type: 'video';
|
|
15
|
+
};
|
|
16
|
+
export type Gravity = 'center' | 'north' | 'northeast' | 'east' | 'southeast' | 'south' | 'southwest' | 'west' | 'northwest';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export declare const fetchers: (f: typeof fetch) => {
|
|
2
2
|
notifications: {
|
|
3
3
|
listTopics: (params?: Partial<import("./utils/api").PagingParams>) => Promise<import("./utils/api").ApiResponse<import("./endpoints/notification").TopicSummary[]>>;
|
|
4
|
-
subscribeToTopic: (params: import("./endpoints/notification").
|
|
5
|
-
unsubscribeFromTopic: (params: import("./endpoints/notification").
|
|
4
|
+
subscribeToTopic: (params: import("./endpoints/notification").SubscriptionParams) => Promise<import("./utils/api").ApiResponse<unknown>>;
|
|
5
|
+
unsubscribeFromTopic: (params: import("./endpoints/notification").SubscriptionParams) => Promise<import("./utils/api").ApiResponse<unknown>>;
|
|
6
6
|
};
|
|
7
|
+
listCategories: (params?: Partial<import("./utils/api").PagingParams>) => Promise<import("./utils/api").ApiResponse<import("./endpoints/categories").Category[]>>;
|
|
8
|
+
getCategory: (id: string) => Promise<import("./utils/api").ApiResponse<import("./endpoints/categories").Category>>;
|
|
7
9
|
listOauthProviders: (params?: Partial<import("./utils/api").PagingParams>) => Promise<import("./utils/api").ApiResponse<import("./endpoints/oauth").OauthProviderSummary[]>>;
|
|
8
10
|
getOauthUser: () => Promise<import("./utils/api").ApiResponse<import("./endpoints/oauth").OauthUser>>;
|
|
9
11
|
registerDevice: (params: import("./endpoints/device").DeviceParams) => Promise<import("./utils/api").ApiResponse<unknown>>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { accountsFetchers } from './endpoints/accounts';
|
|
2
|
+
import { categoriesFetchers } from './endpoints/categories';
|
|
2
3
|
import { deviceFetchers } from './endpoints/device';
|
|
3
4
|
import { notificationFetchers } from './endpoints/notification';
|
|
4
5
|
import { oauthFetchers } from './endpoints/oauth';
|
|
@@ -7,6 +8,7 @@ export const fetchers = (f) => {
|
|
|
7
8
|
...accountsFetchers(f),
|
|
8
9
|
...deviceFetchers(f),
|
|
9
10
|
...oauthFetchers(f),
|
|
10
|
-
|
|
11
|
+
...categoriesFetchers(f),
|
|
12
|
+
notifications: notificationFetchers(f)
|
|
11
13
|
};
|
|
12
14
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface EmailParams {
|
|
2
|
+
context: 'platform' | 'media';
|
|
3
|
+
to: string;
|
|
4
|
+
subject: string;
|
|
5
|
+
body: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Send an email.
|
|
9
|
+
*
|
|
10
|
+
* Return `true` if the email has been sent. It could take a couple of minutes to be received.
|
|
11
|
+
*/
|
|
12
|
+
export declare function sendEmail(params: EmailParams): Promise<boolean>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { env } from '$env/dynamic/private';
|
|
2
|
+
import { ApiError } from '../utils/api';
|
|
3
|
+
import { getApplicationToken } from "./utils";
|
|
4
|
+
/**
|
|
5
|
+
* Send an email.
|
|
6
|
+
*
|
|
7
|
+
* Return `true` if the email has been sent. It could take a couple of minutes to be received.
|
|
8
|
+
*/
|
|
9
|
+
export async function sendEmail(params) {
|
|
10
|
+
const url = getOmerloEndpoint();
|
|
11
|
+
const accessToken = await getApplicationToken();
|
|
12
|
+
const headers = new Headers({
|
|
13
|
+
'x-omerlo-media-id': env.PRIVATE_OMERLO_MEDIA_ID,
|
|
14
|
+
Authorization: `Bearer ${accessToken}`,
|
|
15
|
+
'Content-Type': 'text/html'
|
|
16
|
+
});
|
|
17
|
+
url.pathname = '/api/media/v1/mail/send';
|
|
18
|
+
url.searchParams.append('context', params.context);
|
|
19
|
+
url.searchParams.append('to', params.to);
|
|
20
|
+
url.searchParams.append('subject', params.subject);
|
|
21
|
+
const resp = await fetch(url, { method: 'POST', headers, body: params.body });
|
|
22
|
+
if (resp.ok) {
|
|
23
|
+
return Promise.resolve(true);
|
|
24
|
+
}
|
|
25
|
+
const payload = await resp.json();
|
|
26
|
+
throw new ApiError(resp.status, payload.error, resp.statusText);
|
|
27
|
+
}
|
|
28
|
+
function getOmerloEndpoint() {
|
|
29
|
+
return new URL(`${env.PRIVATE_OMERLO_PROTOCOL}://${env.PRIVATE_OMERLO_HOST}`);
|
|
30
|
+
}
|
|
@@ -1,27 +1,26 @@
|
|
|
1
|
-
import { error } from
|
|
1
|
+
import { error } from '@sveltejs/kit';
|
|
2
2
|
import { env } from '$env/dynamic/private';
|
|
3
|
-
import { ApiError } from
|
|
4
|
-
import { clearAuthorizationCookies, clearAuthorizationUsingHeader, getAccessTokenFromCookie, getApplicationToken, getRefreshTokenFromCookie, setAuthorizationCookies } from
|
|
5
|
-
import { refresh } from
|
|
3
|
+
import { ApiError } from '../utils/api';
|
|
4
|
+
import { clearAuthorizationCookies, clearAuthorizationUsingHeader, getAccessTokenFromCookie, getApplicationToken, getRefreshTokenFromCookie, setAuthorizationCookies } from './utils';
|
|
5
|
+
import { refresh } from './token';
|
|
6
6
|
// NOTE: inspired by https://sami.website/blog/sveltekit-api-reverse-proxy
|
|
7
7
|
const handleApiProxy = async ({ event, ...tail }) => {
|
|
8
|
+
// NOTE: It's important to reset the port BEFORE settings the host because the host could contains the port
|
|
9
|
+
// Example: localhost:4000
|
|
10
|
+
event.url.port = '';
|
|
8
11
|
event.url.host = env.PRIVATE_OMERLO_HOST;
|
|
9
12
|
event.url.protocol = env.PRIVATE_OMERLO_PROTOCOL;
|
|
10
|
-
event.
|
|
11
|
-
let accessToken = event.locals.accessToken;
|
|
12
|
-
if (!accessToken) {
|
|
13
|
-
accessToken = await getApplicationToken();
|
|
14
|
-
}
|
|
13
|
+
const accessToken = event.locals.accessToken ?? (await getApplicationToken());
|
|
15
14
|
const body = event.request.body;
|
|
16
15
|
const method = event.request.method;
|
|
17
|
-
const headers = new Headers(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
16
|
+
const headers = new Headers({
|
|
17
|
+
'x-omerlo-media-id': env.PRIVATE_OMERLO_MEDIA_ID,
|
|
18
|
+
Authorization: `Bearer ${accessToken}`
|
|
19
|
+
});
|
|
20
|
+
const contentType = event.request.headers.get('content-type');
|
|
21
|
+
if (contentType)
|
|
22
|
+
headers.set('Content-Type', contentType);
|
|
23
|
+
return await fetch(event.url.toString(), { body, headers, method, duplex: 'half' })
|
|
25
24
|
.then(async (resp) => {
|
|
26
25
|
const headers = new Headers();
|
|
27
26
|
if (resp.status === 401 && event.locals.accessToken) {
|
|
@@ -32,12 +31,12 @@ const handleApiProxy = async ({ event, ...tail }) => {
|
|
|
32
31
|
const responseOpts = {
|
|
33
32
|
headers: headers,
|
|
34
33
|
status: resp.status,
|
|
35
|
-
statusText: resp.statusText
|
|
34
|
+
statusText: resp.statusText
|
|
36
35
|
};
|
|
37
36
|
return new Response(resp.body, responseOpts);
|
|
38
37
|
})
|
|
39
38
|
.catch((err) => {
|
|
40
|
-
console.log(
|
|
39
|
+
console.log('Could not proxy API request: ', err);
|
|
41
40
|
error(500, 'Something went wrong');
|
|
42
41
|
});
|
|
43
42
|
};
|
|
@@ -45,13 +44,19 @@ export const proxyHook = async ({ event, resolve }) => {
|
|
|
45
44
|
if (event.url.pathname.startsWith('/api/media/v1')) {
|
|
46
45
|
return await handleApiProxy({ event, resolve });
|
|
47
46
|
}
|
|
47
|
+
// TODO remove once every API will be done in Reader
|
|
48
|
+
if (event.url.pathname.startsWith('/api/publisher')) {
|
|
49
|
+
const mediaId = env.PRIVATE_OMERLO_MEDIA_ID;
|
|
50
|
+
event.url.pathname = event.url.pathname.replace('/api/publisher/', `/api/public/publisher/v2/medias/${mediaId}/`);
|
|
51
|
+
return handleApiProxy({ event, resolve });
|
|
52
|
+
}
|
|
48
53
|
return resolve(event);
|
|
49
54
|
};
|
|
50
55
|
export const handleUserToken = async ({ event, resolve }) => {
|
|
51
56
|
const accessToken = getAccessTokenFromCookie(event.cookies);
|
|
52
57
|
const refreshToken = getRefreshTokenFromCookie(event.cookies);
|
|
53
58
|
const opts = {
|
|
54
|
-
filterSerializedResponseHeaders: (name) => name == 'x-logout'
|
|
59
|
+
filterSerializedResponseHeaders: (name) => name == 'x-logout'
|
|
55
60
|
};
|
|
56
61
|
if (accessToken) {
|
|
57
62
|
event.locals.accessToken = accessToken;
|
|
@@ -4,16 +4,16 @@ export interface exchangeAuthorizationCodeParams {
|
|
|
4
4
|
oauthProviderId: string;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
* Exchange an authorization code for an omerlo's token.
|
|
8
|
+
*/
|
|
9
9
|
export declare function exchangeAuthorizationCode(params: exchangeAuthorizationCodeParams): Promise<OmerloToken>;
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
* Refresh the user's token.
|
|
12
|
+
*/
|
|
13
13
|
export declare function refresh(refreshToken: string): Promise<OmerloToken>;
|
|
14
14
|
/**
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
* Authenticate anonymously a user to get an anonymous token.
|
|
16
|
+
*/
|
|
17
17
|
export declare function getAnonymousToken(scope: string): Promise<OmerloToken>;
|
|
18
18
|
export interface OmerloToken {
|
|
19
19
|
accessToken: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { env } from '$env/dynamic/private';
|
|
2
2
|
import { ApiError } from '../utils/api';
|
|
3
3
|
/**
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
* Exchange an authorization code for an omerlo's token.
|
|
5
|
+
*/
|
|
6
6
|
export async function exchangeAuthorizationCode(params) {
|
|
7
7
|
const url = getTokenEndpoint();
|
|
8
8
|
url.pathname = '/api/media/v1/oauth/token';
|
|
@@ -19,8 +19,8 @@ export async function exchangeAuthorizationCode(params) {
|
|
|
19
19
|
throw new ApiError(resp.status, payload.error, resp.statusText);
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
* Refresh the user's token.
|
|
23
|
+
*/
|
|
24
24
|
export async function refresh(refreshToken) {
|
|
25
25
|
const url = getTokenEndpoint();
|
|
26
26
|
url.pathname = '/api/media/v1/oauth/token';
|
|
@@ -34,8 +34,8 @@ export async function refresh(refreshToken) {
|
|
|
34
34
|
throw new ApiError(resp.status, payload.error, resp.statusText);
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
* Authenticate anonymously a user to get an anonymous token.
|
|
38
|
+
*/
|
|
39
39
|
export async function getAnonymousToken(scope) {
|
|
40
40
|
const url = getTokenEndpoint();
|
|
41
41
|
url.pathname = '/api/media/v1/oauth/token';
|
|
@@ -58,6 +58,6 @@ function parseTokenResponse(data) {
|
|
|
58
58
|
accessToken: data.access_token,
|
|
59
59
|
refreshToken: data.refresh_token,
|
|
60
60
|
expiresIn: data.expires_in,
|
|
61
|
-
tokenType: data.token_type
|
|
61
|
+
tokenType: data.token_type
|
|
62
62
|
};
|
|
63
63
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { UserSession } from
|
|
1
|
+
import type { UserSession } from '../stores/user_session';
|
|
2
2
|
import type { Cookies } from '@sveltejs/kit';
|
|
3
|
-
import { type OmerloToken } from
|
|
3
|
+
import { type OmerloToken } from './token';
|
|
4
4
|
export declare function loadUserSession(f: typeof fetch, cookies: Cookies): Promise<UserSession>;
|
|
5
5
|
export declare function isAuthenticated(cookies: Cookies): boolean;
|
|
6
6
|
export declare function setAuthorizationCookies(cookies: Cookies, token: OmerloToken): void;
|
|
@@ -9,6 +9,6 @@ export declare function clearAuthorizationUsingHeader(headers: Headers): void;
|
|
|
9
9
|
export declare function getAccessTokenFromCookie(cookies: Cookies): string | null;
|
|
10
10
|
export declare function getRefreshTokenFromCookie(cookies: Cookies): string | null;
|
|
11
11
|
/**
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
* Get the token used by the application.
|
|
13
|
+
*/
|
|
14
14
|
export declare function getApplicationToken(): Promise<string>;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { useReader } from
|
|
2
|
-
import { getAnonymousToken, refresh } from
|
|
1
|
+
import { useReader } from '../..';
|
|
2
|
+
import { getAnonymousToken, refresh } from './token';
|
|
3
3
|
export async function loadUserSession(f, cookies) {
|
|
4
4
|
const userSession = { verified: false, authenticated: false, user: null };
|
|
5
5
|
if (isAuthenticated(cookies)) {
|
|
6
6
|
userSession.authenticated = true;
|
|
7
7
|
try {
|
|
8
|
-
const userInfo = await useReader(f)
|
|
8
|
+
const userInfo = await useReader(f)
|
|
9
|
+
.userInfo()
|
|
10
|
+
.then((resp) => resp.data);
|
|
9
11
|
userSession.verified = true;
|
|
10
12
|
userSession.user = userInfo;
|
|
11
13
|
}
|
|
@@ -22,7 +24,11 @@ const accessTokenCookieName = 'access_token';
|
|
|
22
24
|
const refreshTokenCookieName = 'refresh_token';
|
|
23
25
|
export function setAuthorizationCookies(cookies, token) {
|
|
24
26
|
cookies.set('logged_in', 'true', { path: '/', httpOnly: false });
|
|
25
|
-
cookies.set(accessTokenCookieName, token.accessToken, {
|
|
27
|
+
cookies.set(accessTokenCookieName, token.accessToken, {
|
|
28
|
+
httpOnly: true,
|
|
29
|
+
path: '/',
|
|
30
|
+
maxAge: token.expiresIn - 60
|
|
31
|
+
});
|
|
26
32
|
cookies.set(refreshTokenCookieName, token.refreshToken, { httpOnly: true, path: '/' });
|
|
27
33
|
}
|
|
28
34
|
export function clearAuthorizationCookies(cookies) {
|
|
@@ -46,11 +52,11 @@ const applicationToken = {
|
|
|
46
52
|
accessToken: '',
|
|
47
53
|
refreshToken: '',
|
|
48
54
|
expiredAt: 0,
|
|
49
|
-
init: false
|
|
55
|
+
init: false
|
|
50
56
|
};
|
|
51
57
|
/**
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
* Get the token used by the application.
|
|
59
|
+
*/
|
|
54
60
|
export async function getApplicationToken() {
|
|
55
61
|
if (!applicationToken.init) {
|
|
56
62
|
await newApplicationToken();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { UserInfo } from
|
|
2
|
-
import { type Readable } from
|
|
1
|
+
import type { UserInfo } from '../endpoints/accounts';
|
|
2
|
+
import { type Readable } from 'svelte/store';
|
|
3
3
|
export interface UserSession {
|
|
4
4
|
user: UserInfo | null;
|
|
5
5
|
verified: boolean;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { getContext, setContext } from
|
|
2
|
-
import { writable } from
|
|
3
|
-
import { browser } from
|
|
4
|
-
import { useReader } from
|
|
5
|
-
import { invalidate } from
|
|
1
|
+
import { getContext, setContext } from 'svelte';
|
|
2
|
+
import { writable } from 'svelte/store';
|
|
3
|
+
import { browser } from '$app/environment';
|
|
4
|
+
import { useReader } from '../..';
|
|
5
|
+
import { invalidate } from '$app/navigation';
|
|
6
6
|
const anonymousUserSession = {
|
|
7
7
|
user: null,
|
|
8
8
|
verified: false,
|
|
9
|
-
authenticated: false
|
|
9
|
+
authenticated: false
|
|
10
10
|
};
|
|
11
11
|
export function initUserSession(session) {
|
|
12
12
|
const { subscribe, update, set } = writable(session);
|
|
@@ -28,16 +28,18 @@ export function initUserSession(session) {
|
|
|
28
28
|
subscribe,
|
|
29
29
|
handleLogout: () => {
|
|
30
30
|
if (!browser) {
|
|
31
|
-
throw new Error(
|
|
31
|
+
throw new Error('MUST NOT call refresh on user session from server side.');
|
|
32
32
|
}
|
|
33
33
|
invalidate('omerlo:user_session');
|
|
34
34
|
update(updateUserInfo(null, false));
|
|
35
35
|
},
|
|
36
36
|
refresh: async () => {
|
|
37
37
|
if (!browser) {
|
|
38
|
-
throw new Error(
|
|
38
|
+
throw new Error('MUST NOT call refresh on user session from server side.');
|
|
39
39
|
}
|
|
40
|
-
const userInfo = await useReader(fetch)
|
|
40
|
+
const userInfo = await useReader(fetch)
|
|
41
|
+
.userInfo()
|
|
42
|
+
.then((resp) => resp.data);
|
|
41
43
|
update(updateUserInfo(userInfo, true));
|
|
42
44
|
}
|
|
43
45
|
};
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import type { ApiAssocs, ApiData, ApiResponse } from './api';
|
|
2
|
-
type FetchOptions<T> = {
|
|
3
|
-
parser
|
|
4
|
-
queryParams?: ApiData;
|
|
5
|
-
method?: 'get' | 'post' | 'put' | 'delete';
|
|
6
|
-
body?: ApiData;
|
|
7
|
-
headers?: Headers;
|
|
2
|
+
type FetchOptions<T> = Required<DirtyFetchOptions> & {
|
|
3
|
+
parser: (data: ApiData, assocs: ApiAssocs) => T;
|
|
8
4
|
};
|
|
9
|
-
export declare function request<T>(f: typeof fetch, path: string, opts: FetchOptions<T
|
|
5
|
+
export declare function request<T>(f: typeof fetch, path: string, opts: Partial<FetchOptions<T>>): Promise<ApiResponse<T>>;
|
|
10
6
|
type DirtyFetchOptions = {
|
|
11
7
|
queryParams?: ApiData;
|
|
12
|
-
method?: 'get' | 'post' | 'put' | 'delete';
|
|
8
|
+
method?: 'get' | 'post' | 'patch' | 'put' | 'delete';
|
|
13
9
|
body?: ApiData;
|
|
14
10
|
headers?: Headers;
|
|
15
11
|
};
|
|
16
12
|
export declare function dirtyRequest(f: typeof fetch, path: string, opts: DirtyFetchOptions): Promise<Response>;
|
|
13
|
+
/**
|
|
14
|
+
* NOTE: This is for OLD api and will be removed once every API will
|
|
15
|
+
* be developped in Reader.
|
|
16
|
+
*/
|
|
17
|
+
export declare function requestPublisher<T>(f: typeof fetch, path: string, opts: Partial<FetchOptions<T>>): Promise<ApiResponse<T>>;
|
|
18
|
+
export declare function dirtyRequestPublisher(f: typeof fetch, path: string, opts: DirtyFetchOptions): Promise<Response>;
|
|
17
19
|
export {};
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { parseApiResponse } from './api';
|
|
2
2
|
import { BROWSER } from 'esm-env';
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
opts.headers = new Headers();
|
|
3
|
+
function parseRequestOpts(params) {
|
|
4
|
+
const headers = params.headers ?? new Headers();
|
|
5
|
+
const method = params.method ?? 'get';
|
|
6
|
+
if (!headers.has('Accept')) {
|
|
7
|
+
headers.set('Accept', 'application/json');
|
|
8
|
+
}
|
|
9
|
+
if (!headers.has('Content-Type') && ['post', 'put', 'patch'].includes(method)) {
|
|
10
|
+
headers.set('Content-Type', 'application/json');
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
return {
|
|
13
|
+
queryParams: params.queryParams,
|
|
14
|
+
body: JSON.stringify(params.body),
|
|
15
|
+
method: params.method ?? 'get',
|
|
16
|
+
headers: headers,
|
|
17
|
+
parser: params.parser || ((data) => data)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export async function request(f, path, opts) {
|
|
21
|
+
const { body, headers, method, queryParams, parser } = parseRequestOpts(opts);
|
|
15
22
|
return dirtyRequest(f, path, { body, headers, method, queryParams }).then(async (resp) => {
|
|
16
23
|
return parseApiResponse(resp, parser);
|
|
17
24
|
});
|
|
@@ -34,3 +41,24 @@ export async function dirtyRequest(f, path, opts) {
|
|
|
34
41
|
}
|
|
35
42
|
return resp;
|
|
36
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* NOTE: This is for OLD api and will be removed once every API will
|
|
46
|
+
* be developped in Reader.
|
|
47
|
+
*/
|
|
48
|
+
export async function requestPublisher(f, path, opts) {
|
|
49
|
+
const { body, headers, method, queryParams, parser } = parseRequestOpts(opts);
|
|
50
|
+
return dirtyRequestPublisher(f, path, { body, headers, method, queryParams }).then(async (resp) => {
|
|
51
|
+
return parseApiResponse(resp, parser);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export async function dirtyRequestPublisher(f, path, opts) {
|
|
55
|
+
const queryParams = new URLSearchParams();
|
|
56
|
+
path = `/api/publisher/${path}`;
|
|
57
|
+
if (opts?.queryParams) {
|
|
58
|
+
Object.entries(opts.queryParams).forEach(([key, value]) => {
|
|
59
|
+
queryParams.append(key, String(value));
|
|
60
|
+
});
|
|
61
|
+
path = `${path}?${queryParams}`;
|
|
62
|
+
}
|
|
63
|
+
return f(path.toString(), opts);
|
|
64
|
+
}
|
package/eslint.config.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omerlo/omerlo-webkit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run package",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
"description": "This webkit is a wapper arround Omerlo's API to create quickly a website using Omerlo.",
|
|
67
67
|
"main": "eslint.config.js",
|
|
68
|
-
"directories": {
|
|
68
|
+
"directories": {},
|
|
69
69
|
"repository": {
|
|
70
70
|
"type": "git",
|
|
71
71
|
"url": "git+https://github.com/Omerlo-Technologies/omerlo-webkit.git"
|