@simplysm/service-server 13.0.68 → 13.0.70
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 +48 -249
- package/dist/auth/jwt-manager.js +2 -2
- package/dist/auth/jwt-manager.js.map +1 -1
- package/dist/core/define-service.js +2 -2
- package/dist/core/define-service.js.map +1 -1
- package/dist/core/service-executor.js +5 -5
- package/dist/core/service-executor.js.map +1 -1
- package/dist/legacy/v1-auto-update-handler.d.ts +2 -2
- package/dist/legacy/v1-auto-update-handler.js +2 -2
- package/dist/legacy/v1-auto-update-handler.js.map +1 -1
- package/dist/service-server.js +11 -11
- package/dist/service-server.js.map +1 -1
- package/dist/services/auto-update-service.js +1 -1
- package/dist/services/auto-update-service.js.map +1 -1
- package/dist/services/orm-service.js +6 -6
- package/dist/services/orm-service.js.map +1 -1
- package/dist/transport/http/http-request-handler.js +1 -1
- package/dist/transport/http/http-request-handler.js.map +1 -1
- package/dist/transport/http/static-file-handler.js +3 -3
- package/dist/transport/http/upload-handler.js +2 -2
- package/dist/transport/http/upload-handler.js.map +1 -1
- package/dist/transport/socket/service-socket.js +2 -2
- package/dist/transport/socket/service-socket.js.map +1 -1
- package/dist/transport/socket/websocket-handler.d.ts.map +1 -1
- package/dist/transport/socket/websocket-handler.js +11 -9
- package/dist/transport/socket/websocket-handler.js.map +1 -1
- package/dist/utils/config-manager.js +7 -7
- package/dist/utils/config-manager.js.map +1 -1
- package/package.json +9 -9
- package/src/auth/jwt-manager.ts +2 -2
- package/src/core/define-service.ts +2 -2
- package/src/core/service-executor.ts +13 -13
- package/src/legacy/v1-auto-update-handler.ts +8 -8
- package/src/service-server.ts +28 -28
- package/src/services/auto-update-service.ts +1 -1
- package/src/services/orm-service.ts +6 -6
- package/src/transport/http/http-request-handler.ts +5 -5
- package/src/transport/http/static-file-handler.ts +7 -7
- package/src/transport/http/upload-handler.ts +3 -3
- package/src/transport/socket/service-socket.ts +4 -4
- package/src/transport/socket/websocket-handler.ts +12 -10
- package/src/utils/config-manager.ts +11 -11
- package/tests/define-service.spec.ts +85 -0
- package/tests/orm-service.spec.ts +83 -0
- package/tests/service-executor.spec.ts +114 -0
- package/docs/authentication.md +0 -114
- package/docs/built-in-services.md +0 -100
- package/docs/server.md +0 -374
- package/docs/transport.md +0 -273
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import type { QueryDef } from "@simplysm/orm-common";
|
|
3
|
+
|
|
4
|
+
vi.mock("@simplysm/orm-node", () => ({
|
|
5
|
+
createDbConn: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import { createDbConn } from "@simplysm/orm-node";
|
|
9
|
+
import { OrmService } from "../src/services/orm-service";
|
|
10
|
+
|
|
11
|
+
describe("OrmService.executeDefs", () => {
|
|
12
|
+
let mockExecute: ReturnType<typeof vi.fn>;
|
|
13
|
+
let methods: any;
|
|
14
|
+
let connId: number;
|
|
15
|
+
|
|
16
|
+
const twoDefs: QueryDef[] = [
|
|
17
|
+
{
|
|
18
|
+
type: "createTable",
|
|
19
|
+
table: { database: "db", name: "A" },
|
|
20
|
+
columns: [{ name: "id", dataType: { type: "bigint" } }],
|
|
21
|
+
primaryKey: ["id"],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: "createTable",
|
|
25
|
+
table: { database: "db", name: "B" },
|
|
26
|
+
columns: [{ name: "id", dataType: { type: "bigint" } }],
|
|
27
|
+
primaryKey: ["id"],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
mockExecute = vi.fn((queries: string[]) => Promise.resolve(queries.map(() => [])));
|
|
33
|
+
|
|
34
|
+
const mockConn = {
|
|
35
|
+
config: { dialect: "postgresql" as const },
|
|
36
|
+
isConnected: true,
|
|
37
|
+
connect: vi.fn(),
|
|
38
|
+
close: vi.fn(),
|
|
39
|
+
execute: mockExecute,
|
|
40
|
+
executeParametrized: vi.fn(),
|
|
41
|
+
bulkInsert: vi.fn(),
|
|
42
|
+
beginTransaction: vi.fn(),
|
|
43
|
+
commitTransaction: vi.fn(),
|
|
44
|
+
rollbackTransaction: vi.fn(),
|
|
45
|
+
on: vi.fn(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
vi.mocked(createDbConn).mockResolvedValue(mockConn as any);
|
|
49
|
+
|
|
50
|
+
const ctx = {
|
|
51
|
+
socket: { on: vi.fn() },
|
|
52
|
+
getConfig: vi.fn(() =>
|
|
53
|
+
Promise.resolve({
|
|
54
|
+
test: { dialect: "postgresql", host: "localhost", database: "db" },
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
clientName: "test",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
methods = OrmService.factory(ctx as any);
|
|
61
|
+
connId = await methods.connect({ configName: "test" });
|
|
62
|
+
mockExecute.mockClear();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("executes queries individually when options is undefined", async () => {
|
|
66
|
+
const result = await methods.executeDefs(connId, twoDefs);
|
|
67
|
+
|
|
68
|
+
// Should pass 2 separate queries, not 1 combined string
|
|
69
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(mockExecute.mock.calls[0][0]).toHaveLength(2);
|
|
71
|
+
|
|
72
|
+
// Should return one result per def
|
|
73
|
+
expect(result).toHaveLength(2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("combines queries when options is explicitly all-null", async () => {
|
|
77
|
+
await methods.executeDefs(connId, twoDefs, [undefined, undefined]);
|
|
78
|
+
|
|
79
|
+
// Should pass 1 combined query string
|
|
80
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(mockExecute.mock.calls[0][0]).toHaveLength(1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { runServiceMethod } from "../src/core/service-executor";
|
|
3
|
+
import { defineService, auth } from "../src/core/define-service";
|
|
4
|
+
|
|
5
|
+
// Minimal mock server
|
|
6
|
+
function createMockServer(services: any[]) {
|
|
7
|
+
return { options: { services, auth: { jwtSecret: "test" } } } as any;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("runServiceMethod with ServiceDefinition", () => {
|
|
11
|
+
it("executes a basic service method", async () => {
|
|
12
|
+
const EchoService = defineService("Echo", (_ctx) => ({
|
|
13
|
+
echo: (msg: string) => `Echo: ${msg}`,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const server = createMockServer([EchoService]);
|
|
17
|
+
const result = await runServiceMethod(server, {
|
|
18
|
+
serviceName: "Echo",
|
|
19
|
+
methodName: "echo",
|
|
20
|
+
params: ["hello"],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result).toBe("Echo: hello");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("throws error when service not found", async () => {
|
|
27
|
+
const server = createMockServer([]);
|
|
28
|
+
|
|
29
|
+
await expect(
|
|
30
|
+
runServiceMethod(server, { serviceName: "Unknown", methodName: "test", params: [] }),
|
|
31
|
+
).rejects.toThrow("Service [Unknown] not found.");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("throws error when method not found", async () => {
|
|
35
|
+
const svc = defineService("Test", (_ctx) => ({
|
|
36
|
+
existing: () => "ok",
|
|
37
|
+
}));
|
|
38
|
+
const server = createMockServer([svc]);
|
|
39
|
+
|
|
40
|
+
await expect(
|
|
41
|
+
runServiceMethod(server, { serviceName: "Test", methodName: "nonexistent", params: [] }),
|
|
42
|
+
).rejects.toThrow("Method [Test.nonexistent] not found.");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("blocks unauthenticated access to auth-required service", async () => {
|
|
46
|
+
const svc = defineService(
|
|
47
|
+
"Protected",
|
|
48
|
+
auth((_ctx) => ({
|
|
49
|
+
secret: () => "secret",
|
|
50
|
+
})),
|
|
51
|
+
);
|
|
52
|
+
const server = createMockServer([svc]);
|
|
53
|
+
|
|
54
|
+
await expect(
|
|
55
|
+
runServiceMethod(server, { serviceName: "Protected", methodName: "secret", params: [] }),
|
|
56
|
+
).rejects.toThrow("Login is required.");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("blocks unauthorized role access", async () => {
|
|
60
|
+
const svc = defineService(
|
|
61
|
+
"Admin",
|
|
62
|
+
auth((_ctx) => ({
|
|
63
|
+
manage: auth(["admin"], () => "managed"),
|
|
64
|
+
view: () => "viewed",
|
|
65
|
+
})),
|
|
66
|
+
);
|
|
67
|
+
const server = createMockServer([svc]);
|
|
68
|
+
|
|
69
|
+
// Has auth but wrong role
|
|
70
|
+
await expect(
|
|
71
|
+
runServiceMethod(server, {
|
|
72
|
+
serviceName: "Admin",
|
|
73
|
+
methodName: "manage",
|
|
74
|
+
params: [],
|
|
75
|
+
http: { clientName: "test", authTokenPayload: { roles: ["user"], data: {} } as any },
|
|
76
|
+
}),
|
|
77
|
+
).rejects.toThrow("Insufficient permissions.");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("allows access with correct role", async () => {
|
|
81
|
+
const svc = defineService(
|
|
82
|
+
"Admin",
|
|
83
|
+
auth((_ctx) => ({
|
|
84
|
+
manage: auth(["admin"], () => "managed"),
|
|
85
|
+
})),
|
|
86
|
+
);
|
|
87
|
+
const server = createMockServer([svc]);
|
|
88
|
+
|
|
89
|
+
const result = await runServiceMethod(server, {
|
|
90
|
+
serviceName: "Admin",
|
|
91
|
+
methodName: "manage",
|
|
92
|
+
params: [],
|
|
93
|
+
http: { clientName: "test", authTokenPayload: { roles: ["admin"], data: {} } as any },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result).toBe("managed");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("provides context to factory", async () => {
|
|
100
|
+
const svc = defineService("Ctx", (ctx) => ({
|
|
101
|
+
getClientName: () => ctx.clientName,
|
|
102
|
+
}));
|
|
103
|
+
const server = createMockServer([svc]);
|
|
104
|
+
|
|
105
|
+
const result = await runServiceMethod(server, {
|
|
106
|
+
serviceName: "Ctx",
|
|
107
|
+
methodName: "getClientName",
|
|
108
|
+
params: [],
|
|
109
|
+
http: { clientName: "my-app", authTokenPayload: undefined },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(result).toBe("my-app");
|
|
113
|
+
});
|
|
114
|
+
});
|
package/docs/authentication.md
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
# Authentication
|
|
2
|
-
|
|
3
|
-
## auth() Wrapper
|
|
4
|
-
|
|
5
|
-
Use the `auth()` wrapper to set authentication requirements on services or methods. Only works when `ServiceServerOptions.auth` is configured.
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { defineService, auth } from "@simplysm/service-server";
|
|
9
|
-
|
|
10
|
-
// Service-level auth: all methods require login
|
|
11
|
-
export const UserService = defineService("User", auth((ctx) => ({
|
|
12
|
-
// Login only required (inherits from service level)
|
|
13
|
-
getProfile: async (): Promise<unknown> => {
|
|
14
|
-
const userId = (ctx.authInfo as { userId: number; role: string })?.userId;
|
|
15
|
-
// ...
|
|
16
|
-
},
|
|
17
|
-
|
|
18
|
-
// Method-level auth: specific role required (overrides service level)
|
|
19
|
-
deleteUser: auth(["admin"], async (targetId: number): Promise<void> => {
|
|
20
|
-
// Only users with admin role can call
|
|
21
|
-
}),
|
|
22
|
-
})));
|
|
23
|
-
|
|
24
|
-
// No authentication required (no auth wrapper)
|
|
25
|
-
export const PublicService = defineService("Public", (ctx) => ({
|
|
26
|
-
healthCheck: async (): Promise<string> => {
|
|
27
|
-
return "OK";
|
|
28
|
-
},
|
|
29
|
-
}));
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Auth behavior:
|
|
33
|
-
|
|
34
|
-
| Target | `auth(factory)` | `auth(["admin"], fn)` |
|
|
35
|
-
|-----------|----------------|-------------------------|
|
|
36
|
-
| Service | All methods require login | All methods require admin role |
|
|
37
|
-
| Method | Method requires login | Method requires admin role |
|
|
38
|
-
| None | No auth required (Public) | - |
|
|
39
|
-
|
|
40
|
-
Method-level `auth()` overrides service-level settings.
|
|
41
|
-
|
|
42
|
-
## getServiceAuthPermissions
|
|
43
|
-
|
|
44
|
-
Read auth permissions from an `auth()`-wrapped function. Returns `undefined` if the function is not wrapped with `auth()`. Primarily used internally by `runServiceMethod`, but exported for advanced use cases.
|
|
45
|
-
|
|
46
|
-
```typescript
|
|
47
|
-
import { defineService, auth, getServiceAuthPermissions } from "@simplysm/service-server";
|
|
48
|
-
|
|
49
|
-
// For a method with auth wrapper
|
|
50
|
-
const methodPerms = getServiceAuthPermissions(someMethod);
|
|
51
|
-
// string[] if permissions are set, or undefined for public (no auth wrapper)
|
|
52
|
-
|
|
53
|
-
// For a service definition
|
|
54
|
-
const servicePerms = someServiceDef.authPermissions;
|
|
55
|
-
// string[] if service-level auth is set, or undefined for public
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
| Signature | Returns | Description |
|
|
59
|
-
|-----------|---------|------|
|
|
60
|
-
| `getServiceAuthPermissions(fn: Function)` | `string[] \| undefined` | Returns the permission array attached by `auth()`, or `undefined` if not wrapped |
|
|
61
|
-
|
|
62
|
-
## JWT Functions
|
|
63
|
-
|
|
64
|
-
Standalone functions for JWT token generation and verification using the `jose` library (HS256 algorithm, 12-hour expiration).
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
import { signJwt, verifyJwt, decodeJwt } from "@simplysm/service-server";
|
|
68
|
-
|
|
69
|
-
// Generate token (12-hour expiration, HS256 algorithm)
|
|
70
|
-
const token = await signJwt("my-secret-key", {
|
|
71
|
-
roles: ["admin", "user"],
|
|
72
|
-
data: { userId: 1, name: "John" },
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Verify token (throws on expiry or invalid signature)
|
|
76
|
-
const payload = await verifyJwt("my-secret-key", token);
|
|
77
|
-
// payload.roles: ["admin", "user"]
|
|
78
|
-
// payload.data: { userId: 1, name: "John" }
|
|
79
|
-
|
|
80
|
-
// Decode token without verification (synchronous, no secret needed)
|
|
81
|
-
const decoded = decodeJwt(token);
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
| Function | Returns | Description |
|
|
85
|
-
|----------|---------|------|
|
|
86
|
-
| `signJwt<TAuthInfo>(jwtSecret, payload)` | `Promise<string>` | Generate a JWT token (HS256, 12-hour expiration) |
|
|
87
|
-
| `verifyJwt<TAuthInfo>(jwtSecret, token)` | `Promise<AuthTokenPayload<TAuthInfo>>` | Verify token signature and expiration, return payload. Throws on invalid or expired tokens |
|
|
88
|
-
| `decodeJwt<TAuthInfo>(token)` | `AuthTokenPayload<TAuthInfo>` | Decode token without verification (synchronous) |
|
|
89
|
-
|
|
90
|
-
In most cases, use `ServiceServer` methods instead of calling these functions directly:
|
|
91
|
-
|
|
92
|
-
```typescript
|
|
93
|
-
// Generate token via server (preferred)
|
|
94
|
-
const token = await server.generateAuthToken({
|
|
95
|
-
roles: ["admin"],
|
|
96
|
-
data: { userId: 1 },
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Verify token via server (preferred)
|
|
100
|
-
const payload = await server.verifyAuthToken(token);
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
## AuthTokenPayload
|
|
104
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
import type { AuthTokenPayload } from "@simplysm/service-server";
|
|
107
|
-
|
|
108
|
-
interface AuthTokenPayload<TAuthInfo = unknown> extends JWTPayload {
|
|
109
|
-
/** User role list (used for permission check in auth wrapper) */
|
|
110
|
-
roles: string[];
|
|
111
|
-
/** Custom auth info (generic type) */
|
|
112
|
-
data: TAuthInfo;
|
|
113
|
-
}
|
|
114
|
-
```
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# Built-in Services
|
|
2
|
-
|
|
3
|
-
## OrmService
|
|
4
|
-
|
|
5
|
-
Provides database connection/query/transaction via WebSocket. The `auth()` wrapper is applied, requiring login.
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
import { createServiceServer, OrmService } from "@simplysm/service-server";
|
|
9
|
-
|
|
10
|
-
const server = createServiceServer({
|
|
11
|
-
port: 8080,
|
|
12
|
-
rootPath: "/app/data",
|
|
13
|
-
auth: { jwtSecret: "secret" },
|
|
14
|
-
services: [OrmService],
|
|
15
|
-
});
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Define ORM config in `.config.json`:
|
|
19
|
-
|
|
20
|
-
```json
|
|
21
|
-
{
|
|
22
|
-
"orm": {
|
|
23
|
-
"default": {
|
|
24
|
-
"dialect": "mysql",
|
|
25
|
-
"host": "localhost",
|
|
26
|
-
"port": 3306,
|
|
27
|
-
"database": "mydb",
|
|
28
|
-
"user": "root",
|
|
29
|
-
"password": "password"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Methods provided by `OrmService`:
|
|
36
|
-
|
|
37
|
-
| Method | Returns | Description |
|
|
38
|
-
|--------|---------|------|
|
|
39
|
-
| `getInfo(opt)` | `Promise<{ dialect, database?, schema? }>` | Query DB connection info |
|
|
40
|
-
| `connect(opt)` | `Promise<number>` | Create DB connection. Returns connection ID |
|
|
41
|
-
| `close(connId)` | `Promise<void>` | Close DB connection |
|
|
42
|
-
| `beginTransaction(connId, isolationLevel?)` | `Promise<void>` | Begin transaction |
|
|
43
|
-
| `commitTransaction(connId)` | `Promise<void>` | Commit transaction |
|
|
44
|
-
| `rollbackTransaction(connId)` | `Promise<void>` | Rollback transaction |
|
|
45
|
-
| `executeParametrized(connId, query, params?)` | `Promise<unknown[][]>` | Execute parameterized query |
|
|
46
|
-
| `executeDefs(connId, defs, options?)` | `Promise<unknown[][]>` | Execute QueryDef-based queries |
|
|
47
|
-
| `bulkInsert(connId, tableName, columnDefs, records)` | `Promise<void>` | Bulk INSERT |
|
|
48
|
-
|
|
49
|
-
When a WebSocket connection is closed, all DB connections opened from that socket are automatically cleaned up.
|
|
50
|
-
|
|
51
|
-
Use `OrmServiceType` to share method signatures with the client:
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
import type { OrmServiceType } from "@simplysm/service-server";
|
|
55
|
-
// OrmServiceType = ServiceMethods<typeof OrmService>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## AutoUpdateService
|
|
59
|
-
|
|
60
|
-
Supports auto-update for client apps. Searches for the latest version files by platform in the client directory. No auth is required.
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
import { createServiceServer, AutoUpdateService } from "@simplysm/service-server";
|
|
64
|
-
|
|
65
|
-
const server = createServiceServer({
|
|
66
|
-
port: 8080,
|
|
67
|
-
rootPath: "/app/data",
|
|
68
|
-
services: [AutoUpdateService],
|
|
69
|
-
});
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Update file structure:
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
rootPath/www/{clientName}/{platform}/updates/
|
|
76
|
-
1.0.0.exe (Windows)
|
|
77
|
-
1.0.1.exe
|
|
78
|
-
1.0.0.apk (Android)
|
|
79
|
-
1.0.1.apk
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
| Method | Returns | Description |
|
|
83
|
-
|--------|---------|------|
|
|
84
|
-
| `getLastVersion(platform)` | `Promise<{ version: string; downloadPath: string } \| undefined>` | Returns the latest version and download path for the platform. Returns `undefined` if no update files exist |
|
|
85
|
-
|
|
86
|
-
Return value example:
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
{
|
|
90
|
-
version: "1.0.1",
|
|
91
|
-
downloadPath: "/my-app/android/updates/1.0.1.apk",
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Use `AutoUpdateServiceType` to share method signatures with the client:
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
import type { AutoUpdateServiceType } from "@simplysm/service-server";
|
|
99
|
-
// AutoUpdateServiceType = ServiceMethods<typeof AutoUpdateService>
|
|
100
|
-
```
|