Skip to content
This repository was archived by the owner on Oct 4, 2024. It is now read-only.

Commit 63037d5

Browse files
committed
improve package for jsr
1 parent 3bbe4c4 commit 63037d5

File tree

11 files changed

+124
-63
lines changed

11 files changed

+124
-63
lines changed

packages/app-server-sdk/README.md

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,47 @@
1-
# shopware-app-server
1+
# App Server
22

3-
This package provides the indepenend runtime for the Shopware App Server. It can be used to run the app server in any JS environment.
3+
This package can be used to create a Shopware App Backend. It's build independent of any JavaScript framework. It relies on Fetch-standardized Request and Response objects.
44

5-
For Cloudflare install the `@friendsofshopware/app-server-cloudflare` package.
6-
For Express install the `@friendsofshopware/app-server-express` package.
5+
## Example with Bun
6+
7+
```js
8+
import { AppServer, InMemoryShopRepository } from '@friendsofshopware/app-server'
9+
10+
const app = new AppServer({
11+
appName: 'MyApp',
12+
appSecret: 'my-secret',
13+
authorizeCallbackUrl: 'http://localhost:3000/authorize/callback',
14+
}, new InMemoryShopRepository());
15+
16+
const server = Bun.serve({
17+
port: 3000,
18+
async fetch(request) {
19+
const { pathname } = new URL(request.url);
20+
if (pathname === '/authorize') {
21+
return app.registration.authorize(request);
22+
} else if (pathname === '/authorize/callback') {
23+
return app.registration.authorizeCallback(request);
24+
} else if (pathname === '/app/product') {
25+
const context = await app.contextResolver.fromSource(request);
26+
27+
// do something with payload, and http client
28+
const resp = new Response(JSON.stringify({
29+
actionType: "notification",
30+
payload: {
31+
status: "success",
32+
message: "Product created",
33+
}
34+
}));
35+
36+
// sign the response, with the shop secret
37+
await app.signer.signResponse(resp, context.shop.getShopSecret());
38+
39+
return resp;
40+
}
41+
42+
return new Response('Not found', { status: 404 });
43+
},
44+
});
45+
46+
console.log(`Listening on localhost:${server.port}`);
47+
```

packages/app-server-sdk/app.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ import { WebCryptoHmacSigner } from "./signer.ts";
33
import { ShopRepositoryInterface } from "./repository.ts";
44
import { ContextResolver } from "./context-resolver.ts";
55

6+
/**
7+
* AppServer is the main class, this is where you start your app
8+
*/
69
export class AppServer {
710
public registration: Registration;
811
public contextResolver: ContextResolver;
912
public signer: WebCryptoHmacSigner;
1013

1114
constructor(
12-
public cfg: AppConfigurationInterface,
15+
public cfg: Configuration,
1316
public repository: ShopRepositoryInterface
1417
) {
1518
this.registration = new Registration(this);
@@ -18,7 +21,7 @@ export class AppServer {
1821
}
1922
}
2023

21-
export interface AppConfigurationInterface {
24+
interface Configuration {
2225
/**
2326
* Your app name
2427
*/

packages/app-server-sdk/context-resolver.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ import { AppServer } from "./app.ts";
22
import { HttpClient } from "./http-client.ts";
33
import { ShopInterface } from "./repository.ts";
44

5+
/**
6+
* ContextResolver is a helper class to create a Context object from a request.
7+
* The context contains the shop, the payload and an instance of the HttpClient
8+
*/
59
export class ContextResolver {
610
constructor(private app: AppServer) {}
711

12+
/**
13+
* Create a context from a request bodty
14+
*/
815
public async fromSource(req: Request): Promise<Context> {
916
const webHookContent = await req.text();
1017
const webHookBody = JSON.parse(webHookContent);
@@ -26,6 +33,10 @@ export class ContextResolver {
2633
return new Context(shop, webHookBody, new HttpClient(shop));
2734
}
2835

36+
/**
37+
* Create a context from a request query parameters
38+
* This is usually a module request from the shopware admin
39+
*/
2940
public async fromModule(req: Request): Promise<Context> {
3041
const url = new URL(req.url);
3142

@@ -50,6 +61,9 @@ export class ContextResolver {
5061
}
5162
}
5263

64+
/**
65+
* Context is the parsed data from the request
66+
*/
5367
export class Context {
5468
constructor(
5569
public shop: ShopInterface,

packages/app-server-sdk/http-client.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { ShopInterface } from "./repository.ts";
22

3+
/**
4+
* HttpClient is a simple wrapper around the fetch API, pre-configured with the shop's URL and access token
5+
*/
36
export class HttpClient {
47
private storage: { expiresIn: Date | null; token: string | null };
58

@@ -10,10 +13,16 @@ export class HttpClient {
1013
};
1114
}
1215

16+
/**
17+
* Permform a GET request
18+
*/
1319
async get(url: string, headers: object = {}): Promise<HttpClientResponse> {
1420
return await this.request("GET", url, null, headers);
1521
}
1622

23+
/**
24+
* Permform a POST request
25+
*/
1726
async post(
1827
url: string,
1928
json: object = {},
@@ -25,6 +34,9 @@ export class HttpClient {
2534
return await this.request("POST", url, JSON.stringify(json), headers);
2635
}
2736

37+
/**
38+
* Permform a PUT request
39+
*/
2840
async put(
2941
url: string,
3042
json: object = {},
@@ -36,6 +48,9 @@ export class HttpClient {
3648
return await this.request("PUT", url, JSON.stringify(json), headers);
3749
}
3850

51+
/**
52+
* Permform a PATCH request
53+
*/
3954
async patch(
4055
url: string,
4156
json: object = {},
@@ -47,6 +62,9 @@ export class HttpClient {
4762
return await this.request("PATCH", url, JSON.stringify(json), headers);
4863
}
4964

65+
/**
66+
* Permform a DELETE request
67+
*/
5068
async delete(
5169
url: string,
5270
json: object = {},
@@ -95,6 +113,9 @@ export class HttpClient {
95113
return new HttpClientResponse(f.status, await f.json(), f.headers);
96114
}
97115

116+
/**
117+
* Obtain a valid bearer token
118+
*/
98119
async getToken(): Promise<string> {
99120
if (this.storage.expiresIn === null) {
100121
const auth = await globalThis.fetch(
@@ -152,6 +173,9 @@ export class HttpClient {
152173
}
153174
}
154175

176+
/**
177+
* HttpClientResponse is the response object of the HttpClient
178+
*/
155179
export class HttpClientResponse {
156180
constructor(
157181
public statusCode: number,

packages/app-server-sdk/jsr.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "@friendsofshopware/app-server",
3+
"version": "0.0.48",
4+
"exports": "./mod.ts"
5+
}

packages/app-server-sdk/mod.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
export { AppServer } from "./app.ts";
2-
export type { AppConfigurationInterface } from "./app.ts";
32
export { InMemoryShopRepository, SimpleShop, DenoKVRepository } from "./repository.ts"
43
export type { ShopInterface, ShopRepositoryInterface } from "./repository.ts"
54
export { HttpClient, HttpClientResponse, ApiClientAuthenticationFailed, ApiClientRequestFailed } from "./http-client.ts"
6-
export { WebCryptoHmacSigner } from "./signer.ts"
75
export { Context } from "./context-resolver.ts"

packages/app-server-sdk/registration.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { AppServer } from "./app.ts";
33
export class Registration {
44
constructor(private app: AppServer) {}
55

6+
/**
7+
* This method checks the request for the handshake with the Shopware Shop.
8+
* if it's valid a Shop will be created, and a proof will be responded with a confirmation url.
9+
* then the Shop will call the confirmation url, and this should be handled by the authorizeCallback method to finish the handshake.
10+
*/
611
public async authorize(req: Request): Promise<Response> {
712
const url = new URL(req.url);
813

@@ -52,6 +57,10 @@ export class Registration {
5257
);
5358
}
5459

60+
/**
61+
* This method is called by the Shopware Shop to confirm the handshake.
62+
* It will update the shop with the given oauth2 credentials.
63+
*/
5564
public async authorizeCallback(
5665
req: Request
5766
): Promise<Response> {

packages/app-server-sdk/repository.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* ShopInterface defines the object that given back from the ShopRepository, it should methods to get the shop data and set them
3+
*/
14
export interface ShopInterface {
25
getShopId(): string;
36
getShopUrl(): string;
@@ -7,6 +10,10 @@ export interface ShopInterface {
710
setShopCredentials(clientId: string, clientSecret: string): void;
811
}
912

13+
/**
14+
* ShopRepositoryInterface is the storage interface for the shops, you should implement this to save the shop data to your database
15+
* For testing cases the InMemoryShopRepository can be used
16+
*/
1017
export interface ShopRepositoryInterface {
1118
createShopStruct(shopId: string, shopUrl: string, shopSecret: string): ShopInterface;
1219

@@ -19,6 +26,9 @@ export interface ShopRepositoryInterface {
1926
deleteShop(id: string): Promise<void>;
2027
}
2128

29+
/**
30+
* SimpleShop is a simple implementation of the ShopInterface, it stores the shop data in memory
31+
*/
2232
export class SimpleShop implements ShopInterface {
2333
private shopId: string;
2434
private shopUrl: string;
@@ -70,7 +80,7 @@ export class InMemoryShopRepository implements ShopRepositoryInterface {
7080
this.storage.set(shop.getShopId(), shop);
7181
}
7282

73-
async getShopById(id: string) {
83+
async getShopById(id: string): Promise<ShopInterface|null> {
7484
if (this.storage.has(id)) {
7585
return this.storage.get(id) as ShopInterface;
7686
}
@@ -87,6 +97,9 @@ export class InMemoryShopRepository implements ShopRepositoryInterface {
8797
}
8898
}
8999

100+
/**
101+
* DenoKVRepository is a ShopRepositoryInterface implementation that uses the Deno KV storage to save the shop data
102+
*/
90103
export class DenoKVRepository implements ShopRepositoryInterface {
91104
constructor(private namespace = "shops") {}
92105

@@ -143,4 +156,3 @@ export class DenoKVRepository implements ShopRepositoryInterface {
143156
await kv.delete([this.namespace, id]);
144157
}
145158
}
146-

packages/app-server-sdk/scripts/build.ts

Lines changed: 0 additions & 46 deletions
This file was deleted.

packages/app-server-sdk/signer.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ export class WebCryptoHmacSigner {
1414
);
1515
}
1616

17-
async verifyGetRequest(request: Request, secret: string) {
17+
async verifyGetRequest(request: Request, secret: string): Promise<boolean> {
1818
const url = new URL(request.url);
1919
const signature = url.searchParams.get("shopware-shop-signature") as string;
2020
url.searchParams.delete("shopware-shop-signature");
2121

2222
return await this.verify(signature, url.searchParams.toString(), secret);
2323
}
2424

25-
async getKeyForSecret(secret: string) {
25+
async getKeyForSecret(secret: string): Promise<CryptoKey> {
2626
if (this.keyCache.has(secret)) {
27-
return this.keyCache.get(secret);
27+
return this.keyCache.get(secret) as CryptoKey;
2828
}
2929

3030
const secretKeyData = this.encoder.encode(secret);
@@ -41,7 +41,7 @@ export class WebCryptoHmacSigner {
4141
return key;
4242
}
4343

44-
async sign(message: string, secret: string) {
44+
async sign(message: string, secret: string): Promise<string> {
4545
const key = await this.getKeyForSecret(secret);
4646

4747
const mac = await crypto.subtle.sign(
@@ -53,13 +53,13 @@ export class WebCryptoHmacSigner {
5353
return this.buf2hex(mac);
5454
}
5555

56-
async verify(signature: string, data: string, secret: string) {
56+
async verify(signature: string, data: string, secret: string): Promise<boolean> {
5757
const signed = await this.sign(data, secret);
5858

5959
return signature === signed;
6060
}
6161

62-
buf2hex(buf: any) {
62+
buf2hex(buf: ArrayBuffer): string {
6363
return Array.prototype.map.call(
6464
new Uint8Array(buf),
6565
(x) => (("00" + x.toString(16)).slice(-2)),

0 commit comments

Comments
 (0)