@dooboostore/simple-boot-http-server 1.0.24 → 1.0.26
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 +113 -0
- package/dist/cjs/SimpleBootHttpServer.js +16 -4
- package/dist/cjs/SimpleBootHttpServer.js.map +2 -2
- package/dist/cjs/bundle-entry.js +34 -24
- package/dist/cjs/bundle-entry.js.map +2 -2
- package/dist/cjs/decorators/MethodMapping.js +33 -3
- package/dist/cjs/decorators/MethodMapping.js.map +2 -2
- package/dist/cjs/errors/BadRequestError.js +2 -1
- package/dist/cjs/errors/BadRequestError.js.map +2 -2
- package/dist/cjs/errors/ForbiddenError.js +2 -1
- package/dist/cjs/errors/ForbiddenError.js.map +2 -2
- package/dist/cjs/errors/InternalServerError.js +2 -1
- package/dist/cjs/errors/InternalServerError.js.map +2 -2
- package/dist/cjs/errors/NotFoundError.js +2 -1
- package/dist/cjs/errors/NotFoundError.js.map +2 -2
- package/dist/cjs/errors/UnauthorizedError.js +2 -1
- package/dist/cjs/errors/UnauthorizedError.js.map +2 -2
- package/dist/cjs/filters/IntentSchemeFilter.js +1 -2
- package/dist/cjs/filters/IntentSchemeFilter.js.map +2 -2
- package/dist/cjs/filters/ResourceFilter.js +3 -2
- package/dist/cjs/filters/ResourceFilter.js.map +2 -2
- package/dist/cjs/filters/index.js +3 -1
- package/dist/cjs/filters/index.js.map +2 -2
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/inject/InjectSituationType.js +1 -1
- package/dist/cjs/inject/InjectSituationType.js.map +2 -2
- package/dist/cjs/models/RequestResponse.js +19 -2
- package/dist/cjs/models/RequestResponse.js.map +2 -2
- package/dist/cjs/models/datas/body/index.js +28 -0
- package/dist/cjs/models/datas/body/index.js.map +7 -0
- package/dist/cjs/models/datas/index.js +32 -0
- package/dist/cjs/models/datas/index.js.map +7 -0
- package/dist/cjs/models/datas/{body → path}/ReqPathData.js.map +1 -1
- package/dist/cjs/models/datas/{search → path}/ReqSearchParamsObj.js.map +1 -1
- package/dist/cjs/models/datas/path/index.js +24 -0
- package/dist/cjs/models/datas/path/index.js.map +7 -0
- package/dist/cjs/models/index.js +4 -16
- package/dist/cjs/models/index.js.map +2 -2
- package/dist/cjs/models/method/index.js +22 -0
- package/dist/cjs/models/method/index.js.map +7 -0
- package/dist/cjs/models/method/patch/index.js +22 -0
- package/dist/cjs/models/method/patch/index.js.map +7 -0
- package/dist/cjs/option/HttpServerOption.js.map +2 -2
- package/dist/cjs/proxy/SymbolIntentApiServiceProxy.js +98 -95
- package/dist/cjs/proxy/SymbolIntentApiServiceProxy.js.map +3 -3
- package/dist/cjs/proxy/SymbolIntentApiServiceProxyBase.js +1 -1
- package/dist/cjs/proxy/SymbolIntentApiServiceProxyBase.js.map +2 -2
- package/dist/cjs/proxy/TopicProtocolApiServiceProxy.js +244 -266
- package/dist/cjs/proxy/TopicProtocolApiServiceProxy.js.map +3 -3
- package/dist/cjs/websocket/WebSocketClient.js +2 -1
- package/dist/cjs/websocket/WebSocketClient.js.map +2 -2
- package/dist/esm/SimpleBootHttpServer.js +16 -4
- package/dist/esm/SimpleBootHttpServer.js.map +2 -2
- package/dist/esm/bundle-entry.js +14 -12
- package/dist/esm/bundle-entry.js.map +2 -2
- package/dist/esm/decorators/MethodMapping.js +22 -2
- package/dist/esm/decorators/MethodMapping.js.map +2 -2
- package/dist/esm/errors/BadRequestError.js +2 -1
- package/dist/esm/errors/BadRequestError.js.map +2 -2
- package/dist/esm/errors/ForbiddenError.js +2 -1
- package/dist/esm/errors/ForbiddenError.js.map +2 -2
- package/dist/esm/errors/InternalServerError.js +2 -1
- package/dist/esm/errors/InternalServerError.js.map +2 -2
- package/dist/esm/errors/NotFoundError.js +2 -1
- package/dist/esm/errors/NotFoundError.js.map +2 -2
- package/dist/esm/errors/UnauthorizedError.js +2 -1
- package/dist/esm/errors/UnauthorizedError.js.map +2 -2
- package/dist/esm/filters/IntentSchemeFilter.js +2 -3
- package/dist/esm/filters/IntentSchemeFilter.js.map +2 -2
- package/dist/esm/filters/ResourceFilter.js +3 -2
- package/dist/esm/filters/ResourceFilter.js.map +2 -2
- package/dist/esm/filters/index.js +1 -0
- package/dist/esm/filters/index.js.map +2 -2
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +2 -2
- package/dist/esm/inject/InjectSituationType.js +1 -1
- package/dist/esm/inject/InjectSituationType.js.map +2 -2
- package/dist/esm/models/RequestResponse.js +19 -2
- package/dist/esm/models/RequestResponse.js.map +2 -2
- package/dist/esm/models/datas/body/index.js +5 -0
- package/dist/esm/models/datas/body/index.js.map +7 -0
- package/dist/esm/models/datas/index.js +7 -0
- package/dist/esm/models/datas/index.js.map +7 -0
- package/dist/esm/models/datas/{body → path}/ReqPathData.js.map +1 -1
- package/dist/esm/models/datas/{search → path}/ReqSearchParamsObj.js.map +1 -1
- package/dist/esm/models/datas/path/index.js +3 -0
- package/dist/esm/models/datas/path/index.js.map +7 -0
- package/dist/esm/models/index.js +2 -8
- package/dist/esm/models/index.js.map +2 -2
- package/dist/esm/models/method/index.js +2 -0
- package/dist/esm/models/method/index.js.map +7 -0
- package/dist/esm/models/method/patch/index.js +2 -0
- package/dist/esm/models/method/patch/index.js.map +7 -0
- package/dist/esm/option/HttpServerOption.js.map +2 -2
- package/dist/esm/proxy/SymbolIntentApiServiceProxy.js +98 -95
- package/dist/esm/proxy/SymbolIntentApiServiceProxy.js.map +3 -3
- package/dist/esm/proxy/SymbolIntentApiServiceProxyBase.js +2 -2
- package/dist/esm/proxy/SymbolIntentApiServiceProxyBase.js.map +2 -2
- package/dist/esm/proxy/TopicProtocolApiServiceProxy.js +244 -266
- package/dist/esm/proxy/TopicProtocolApiServiceProxy.js.map +3 -3
- package/dist/esm/websocket/WebSocketClient.js +3 -2
- package/dist/esm/websocket/WebSocketClient.js.map +2 -2
- package/dist/esm-bundle/dooboostore-simple-boot-http-server.esm.js +19374 -4956
- package/dist/esm-bundle/dooboostore-simple-boot-http-server.esm.js.map +4 -4
- package/dist/types/SimpleBootHttpServer.d.ts +1 -1
- package/dist/types/SimpleBootHttpServer.d.ts.map +1 -1
- package/dist/types/bundle-entry.d.ts +7 -12
- package/dist/types/bundle-entry.d.ts.map +1 -1
- package/dist/types/decorators/MethodMapping.d.ts +10 -0
- package/dist/types/decorators/MethodMapping.d.ts.map +1 -1
- package/dist/types/errors/BadRequestError.d.ts +2 -2
- package/dist/types/errors/BadRequestError.d.ts.map +1 -1
- package/dist/types/errors/ForbiddenError.d.ts +2 -2
- package/dist/types/errors/ForbiddenError.d.ts.map +1 -1
- package/dist/types/errors/InternalServerError.d.ts +2 -2
- package/dist/types/errors/InternalServerError.d.ts.map +1 -1
- package/dist/types/errors/NotFoundError.d.ts +2 -2
- package/dist/types/errors/NotFoundError.d.ts.map +1 -1
- package/dist/types/errors/UnauthorizedError.d.ts +2 -2
- package/dist/types/errors/UnauthorizedError.d.ts.map +1 -1
- package/dist/types/filters/ResourceFilter.d.ts.map +1 -1
- package/dist/types/filters/index.d.ts +1 -0
- package/dist/types/filters/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/inject/InjectSituationType.d.ts.map +1 -1
- package/dist/types/models/RequestResponse.d.ts +3 -1
- package/dist/types/models/RequestResponse.d.ts.map +1 -1
- package/dist/types/models/datas/body/index.d.ts +5 -0
- package/dist/types/models/datas/body/index.d.ts.map +1 -0
- package/dist/types/models/datas/index.d.ts +7 -0
- package/dist/types/models/datas/index.d.ts.map +1 -0
- package/dist/types/models/datas/{body → path}/ReqPathData.d.ts.map +1 -1
- package/dist/types/models/datas/path/ReqSearchParamsObj.d.ts.map +1 -0
- package/dist/types/models/datas/path/index.d.ts +3 -0
- package/dist/types/models/datas/path/index.d.ts.map +1 -0
- package/dist/types/models/index.d.ts +2 -8
- package/dist/types/models/index.d.ts.map +1 -1
- package/dist/types/models/method/index.d.ts +2 -0
- package/dist/types/models/method/index.d.ts.map +1 -0
- package/dist/types/models/method/patch/index.d.ts +2 -0
- package/dist/types/models/method/patch/index.d.ts.map +1 -0
- package/dist/types/option/HttpServerOption.d.ts +6 -2
- package/dist/types/option/HttpServerOption.d.ts.map +1 -1
- package/dist/types/proxy/SymbolIntentApiServiceProxy.d.ts +6 -8
- package/dist/types/proxy/SymbolIntentApiServiceProxy.d.ts.map +1 -1
- package/dist/types/proxy/TopicProtocolApiServiceProxy.d.ts +5 -15
- package/dist/types/proxy/TopicProtocolApiServiceProxy.d.ts.map +1 -1
- package/dist/types/websocket/WebSocketClient.d.ts.map +1 -1
- package/dist/umd-bundle/dooboostore-simple-boot-http-server.umd.js +19340 -4928
- package/dist/umd-bundle/dooboostore-simple-boot-http-server.umd.js.map +4 -4
- package/dist/umd-bundle/websocket-client.umd.js +378 -1468
- package/dist/umd-bundle/websocket-client.umd.js.map +4 -4
- package/package.json +8 -8
- package/src/SimpleBootHttpServer.ts +20 -6
- package/src/bundle-entry.ts +12 -12
- package/src/decorators/MethodMapping.ts +13 -1
- package/src/errors/BadRequestError.ts +7 -3
- package/src/errors/ForbiddenError.ts +7 -3
- package/src/errors/InternalServerError.ts +7 -3
- package/src/errors/NotFoundError.ts +7 -3
- package/src/errors/UnauthorizedError.ts +7 -3
- package/src/filters/IntentSchemeFilter.ts +2 -2
- package/src/filters/ResourceFilter.ts +5 -2
- package/src/filters/index.ts +1 -0
- package/src/index.ts +1 -0
- package/src/inject/InjectSituationType.ts +1 -1
- package/src/models/RequestResponse.ts +28 -2
- package/src/models/datas/body/index.ts +4 -0
- package/src/models/datas/index.ts +6 -0
- package/src/models/datas/path/index.ts +2 -0
- package/src/models/index.ts +2 -8
- package/src/models/method/index.ts +1 -0
- package/src/models/method/patch/index.ts +1 -0
- package/src/option/HttpServerOption.ts +4 -2
- package/src/proxy/SymbolIntentApiServiceProxy.ts +103 -97
- package/src/proxy/SymbolIntentApiServiceProxyBase.ts +2 -2
- package/src/proxy/TopicProtocolApiServiceProxy.ts +260 -275
- package/src/websocket/WebSocketClient.ts +3 -2
- package/README.MD +0 -62
- package/dist/types/models/datas/search/ReqSearchParamsObj.d.ts.map +0 -1
- /package/dist/cjs/models/datas/{body → path}/ReqPathData.js +0 -0
- /package/dist/cjs/models/datas/{search → path}/ReqSearchParamsObj.js +0 -0
- /package/dist/esm/models/datas/{body → path}/ReqPathData.js +0 -0
- /package/dist/esm/models/datas/{search → path}/ReqSearchParamsObj.js +0 -0
- /package/dist/types/models/datas/{body → path}/ReqPathData.d.ts +0 -0
- /package/dist/types/models/datas/{search → path}/ReqSearchParamsObj.d.ts +0 -0
- /package/src/models/datas/{body → path}/ReqPathData.ts +0 -0
- /package/src/models/datas/{search → path}/ReqSearchParamsObj.ts +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getSim,
|
|
1
|
+
import { getSim, sim, SimConfig } from '@dooboostore/simple-boot';
|
|
2
2
|
import { HttpHeaders } from '../codes/HttpHeaders';
|
|
3
3
|
import { ApiService } from '@dooboostore/simple-boot';
|
|
4
|
-
import { RandomUtils } from '@dooboostore/core';
|
|
4
|
+
import { ConstructorType, RandomUtils } from '@dooboostore/core';
|
|
5
5
|
import { HttpFetcherTarget } from '@dooboostore/core';
|
|
6
6
|
|
|
7
7
|
export type TopicProtocolConfig<TBody = unknown> = {
|
|
@@ -26,303 +26,288 @@ type TopicProtocolRequest<TBody = unknown> = {
|
|
|
26
26
|
type FileEntry = { id: string; file: Blob; filename?: string };
|
|
27
27
|
type MixedPart = { headers: Record<string, string>; body: Uint8Array };
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
export class TopicProtocolApiServiceProxy<T extends object> implements ProxyHandler<T> {
|
|
31
|
-
constructor(private apiService: ApiService) {}
|
|
29
|
+
// ── Helpers (module-level, mirrors SymbolIntentApiServiceProxy pattern) ──────
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
31
|
+
const isFileValue = (value: unknown): value is File | Blob | ArrayBuffer | Uint8Array => {
|
|
32
|
+
if (typeof File !== 'undefined' && value instanceof File) return true;
|
|
33
|
+
if (typeof Blob !== 'undefined' && value instanceof Blob) return true;
|
|
34
|
+
if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) return true;
|
|
35
|
+
if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) return true;
|
|
36
|
+
return false;
|
|
37
|
+
};
|
|
48
38
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
if (typeof File !== 'undefined' && value instanceof File) {
|
|
54
|
-
const blob = new Blob([value], { type: value.type });
|
|
55
|
-
return { id, file: blob, filename: value.name };
|
|
56
|
-
}
|
|
57
|
-
if (value instanceof Blob) {
|
|
58
|
-
return { id, file: value };
|
|
59
|
-
}
|
|
60
|
-
const view = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
61
|
-
const safe = value instanceof Uint8Array ? new Uint8Array(value) : new Uint8Array(view);
|
|
62
|
-
const blob = new Blob([safe]);
|
|
63
|
-
return { id, file: blob };
|
|
39
|
+
const toFileEntry = (value: File | Blob | ArrayBuffer | Uint8Array, id: string): FileEntry | undefined => {
|
|
40
|
+
if (typeof Blob === 'undefined') return undefined;
|
|
41
|
+
if (typeof File !== 'undefined' && value instanceof File) {
|
|
42
|
+
return { id, file: new Blob([value], { type: value.type }), filename: value.name };
|
|
64
43
|
}
|
|
44
|
+
if (value instanceof Blob) return { id, file: value };
|
|
45
|
+
const safe = value instanceof Uint8Array ? new Uint8Array(value) : new Uint8Array(value);
|
|
46
|
+
return { id, file: new Blob([safe]) };
|
|
47
|
+
};
|
|
65
48
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
49
|
+
const buildRequest = <TBody>(target: string, body?: TBody, type: TopicProtocolType = 'intent') => {
|
|
50
|
+
const fileEntries: FileEntry[] = [];
|
|
51
|
+
let fileIndex = 0;
|
|
69
52
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const replaceFiles = (value: unknown): unknown => {
|
|
81
|
-
if (TopicProtocolApiServiceProxy.isFileValue(value)) {
|
|
82
|
-
const id = registerFile(value);
|
|
83
|
-
if (id) {
|
|
84
|
-
return { $file: id };
|
|
85
|
-
}
|
|
86
|
-
return value;
|
|
87
|
-
}
|
|
88
|
-
if (Array.isArray(value)) {
|
|
89
|
-
return value.map(item => replaceFiles(item));
|
|
90
|
-
}
|
|
91
|
-
if (value && typeof value === 'object') {
|
|
92
|
-
const result: Record<string, unknown> = {};
|
|
93
|
-
Object.keys(value).forEach(key => {
|
|
94
|
-
result[key] = replaceFiles((value as Record<string, unknown>)[key]);
|
|
95
|
-
});
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
return value;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const payload = replaceFiles(body);
|
|
102
|
-
const request: TopicProtocolRequest<TBody> = {
|
|
103
|
-
type,
|
|
104
|
-
target,
|
|
105
|
-
requestUUID: RandomUtils.uuid4(),
|
|
106
|
-
body: payload as TBody,
|
|
107
|
-
files: fileEntries.length > 0 ? fileEntries.map(entry => ({ id: entry.id })) : undefined
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
return { request, fileEntries };
|
|
111
|
-
}
|
|
53
|
+
const registerFile = (value: File | Blob | ArrayBuffer | Uint8Array): string | undefined => {
|
|
54
|
+
const id = `f${++fileIndex}`;
|
|
55
|
+
const entry = toFileEntry(value, id);
|
|
56
|
+
if (entry) { fileEntries.push(entry); return id; }
|
|
57
|
+
return undefined;
|
|
58
|
+
};
|
|
112
59
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return undefined;
|
|
60
|
+
const replaceFiles = (value: unknown): unknown => {
|
|
61
|
+
if (isFileValue(value)) {
|
|
62
|
+
const id = registerFile(value);
|
|
63
|
+
return id ? { $file: id } : value;
|
|
118
64
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const indexOf = (source: Uint8Array, needle: Uint8Array, from = 0) => {
|
|
126
|
-
for (let i = from; i <= source.length - needle.length; i++) {
|
|
127
|
-
let matched = true;
|
|
128
|
-
for (let j = 0; j < needle.length; j++) {
|
|
129
|
-
if (source[i + j] !== needle[j]) {
|
|
130
|
-
matched = false;
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (matched) {
|
|
135
|
-
return i;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return -1;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const parts: MixedPart[] = [];
|
|
142
|
-
let cursor = indexOf(buffer, boundaryBytes, 0);
|
|
143
|
-
while (cursor !== -1) {
|
|
144
|
-
const nextBoundary = indexOf(buffer, boundaryBytes, cursor + boundaryBytes.length);
|
|
145
|
-
const endBoundary = indexOf(buffer, endBoundaryBytes, cursor + boundaryBytes.length);
|
|
146
|
-
const isFinal = endBoundary !== -1 && (endBoundary < nextBoundary || nextBoundary === -1);
|
|
147
|
-
const partStart = cursor + boundaryBytes.length;
|
|
148
|
-
if (isFinal) {
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
let headerStart = partStart;
|
|
152
|
-
if (buffer[headerStart] === 13 && buffer[headerStart + 1] === 10) {
|
|
153
|
-
headerStart += 2;
|
|
154
|
-
}
|
|
155
|
-
const headerEnd = indexOf(buffer, separator, headerStart);
|
|
156
|
-
if (headerEnd === -1) {
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
const headerText = new TextDecoder().decode(buffer.slice(headerStart, headerEnd));
|
|
160
|
-
const headers: Record<string, string> = {};
|
|
161
|
-
headerText.split('\r\n').forEach(line => {
|
|
162
|
-
const idx = line.indexOf(':');
|
|
163
|
-
if (idx > -1) {
|
|
164
|
-
const key = line.slice(0, idx).trim().toLowerCase();
|
|
165
|
-
headers[key] = line.slice(idx + 1).trim();
|
|
166
|
-
}
|
|
65
|
+
if (Array.isArray(value)) return value.map(item => replaceFiles(item));
|
|
66
|
+
if (value && typeof value === 'object') {
|
|
67
|
+
const result: Record<string, unknown> = {};
|
|
68
|
+
Object.keys(value).forEach(key => {
|
|
69
|
+
result[key] = replaceFiles((value as Record<string, unknown>)[key]);
|
|
167
70
|
});
|
|
168
|
-
|
|
169
|
-
const bodyEnd = nextBoundary === -1 ? buffer.length : nextBoundary - 2;
|
|
170
|
-
const body = buffer.slice(bodyStart, bodyEnd);
|
|
171
|
-
parts.push({ headers, body });
|
|
172
|
-
cursor = nextBoundary;
|
|
71
|
+
return result;
|
|
173
72
|
}
|
|
73
|
+
return value;
|
|
74
|
+
};
|
|
174
75
|
|
|
175
|
-
|
|
176
|
-
|
|
76
|
+
const payload = replaceFiles(body);
|
|
77
|
+
const request: TopicProtocolRequest<TBody> = {
|
|
78
|
+
type,
|
|
79
|
+
target,
|
|
80
|
+
requestUUID: RandomUtils.uuid4(),
|
|
81
|
+
body: payload as TBody,
|
|
82
|
+
files: fileEntries.length > 0 ? fileEntries.map(e => ({ id: e.id })) : undefined,
|
|
83
|
+
};
|
|
177
84
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return undefined;
|
|
181
|
-
}
|
|
182
|
-
const match = disposition.match(/filename=\"?([^\";]+)\"?/i);
|
|
183
|
-
return match ? match[1] : undefined;
|
|
184
|
-
}
|
|
85
|
+
return { request, fileEntries };
|
|
86
|
+
};
|
|
185
87
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
88
|
+
const parseDispositionName = (disposition?: string) => {
|
|
89
|
+
const match = disposition?.match(/name=\"?([^\";]+)\"?/i);
|
|
90
|
+
return match ? match[1] : undefined;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const parseDispositionFilename = (disposition?: string) => {
|
|
94
|
+
const match = disposition?.match(/filename=\"?([^\";]+)\"?/i);
|
|
95
|
+
return match ? match[1] : undefined;
|
|
96
|
+
};
|
|
193
97
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
98
|
+
const parseMultipartMixed = async (response: Response) => {
|
|
99
|
+
const contentType = response.headers.get('Content-Type') ?? '';
|
|
100
|
+
const boundaryMatch = contentType.match(/boundary=([^;]+)/i);
|
|
101
|
+
if (!boundaryMatch) return undefined;
|
|
102
|
+
|
|
103
|
+
const boundary = boundaryMatch[1].replace(/^"|"$/g, '');
|
|
104
|
+
const buffer = new Uint8Array(await response.arrayBuffer());
|
|
105
|
+
const boundaryBytes = new TextEncoder().encode(`--${boundary}`);
|
|
106
|
+
const endBoundaryBytes = new TextEncoder().encode(`--${boundary}--`);
|
|
107
|
+
const separator = new TextEncoder().encode('\r\n\r\n');
|
|
108
|
+
|
|
109
|
+
const indexOf = (source: Uint8Array, needle: Uint8Array, from = 0) => {
|
|
110
|
+
for (let i = from; i <= source.length - needle.length; i++) {
|
|
111
|
+
let matched = true;
|
|
112
|
+
for (let j = 0; j < needle.length; j++) {
|
|
113
|
+
if (source[i + j] !== needle[j]) { matched = false; break; }
|
|
202
114
|
}
|
|
115
|
+
if (matched) return i;
|
|
203
116
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
117
|
+
return -1;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const parts: MixedPart[] = [];
|
|
121
|
+
let cursor = indexOf(buffer, boundaryBytes, 0);
|
|
122
|
+
|
|
123
|
+
while (cursor !== -1) {
|
|
124
|
+
const nextBoundary = indexOf(buffer, boundaryBytes, cursor + boundaryBytes.length);
|
|
125
|
+
const endBoundary = indexOf(buffer, endBoundaryBytes, cursor + boundaryBytes.length);
|
|
126
|
+
const isFinal = endBoundary !== -1 && (endBoundary < nextBoundary || nextBoundary === -1);
|
|
127
|
+
const partStart = cursor + boundaryBytes.length;
|
|
128
|
+
if (isFinal) break;
|
|
129
|
+
|
|
130
|
+
let headerStart = partStart;
|
|
131
|
+
if (buffer[headerStart] === 13 && buffer[headerStart + 1] === 10) headerStart += 2;
|
|
132
|
+
|
|
133
|
+
const headerEnd = indexOf(buffer, separator, headerStart);
|
|
134
|
+
if (headerEnd === -1) break;
|
|
135
|
+
|
|
136
|
+
const headerText = new TextDecoder().decode(buffer.slice(headerStart, headerEnd));
|
|
137
|
+
const headers: Record<string, string> = {};
|
|
138
|
+
headerText.split('\r\n').forEach(line => {
|
|
139
|
+
const idx = line.indexOf(':');
|
|
140
|
+
if (idx > -1) headers[line.slice(0, idx).trim().toLowerCase()] = line.slice(idx + 1).trim();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const bodyStart = headerEnd + separator.length;
|
|
144
|
+
const bodyEnd = nextBoundary === -1 ? buffer.length : nextBoundary - 2;
|
|
145
|
+
parts.push({ headers, body: buffer.slice(bodyStart, bodyEnd) });
|
|
146
|
+
cursor = nextBoundary;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { boundary, parts };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const replaceFileRefs = (
|
|
153
|
+
value: unknown,
|
|
154
|
+
fileMap: Map<string, { name?: string; mime?: string; size: number; buffer: ArrayBuffer }>,
|
|
155
|
+
): unknown => {
|
|
156
|
+
if (value && typeof value === 'object' && '$file' in value) {
|
|
157
|
+
const fileKey = (value as { $file?: unknown }).$file;
|
|
158
|
+
if (typeof fileKey === 'string') {
|
|
159
|
+
const file = fileMap.get(fileKey);
|
|
160
|
+
if (file) return file;
|
|
213
161
|
}
|
|
214
|
-
return value;
|
|
215
162
|
}
|
|
163
|
+
if (Array.isArray(value)) return value.map(item => replaceFileRefs(item, fileMap));
|
|
164
|
+
if (value && typeof value === 'object') {
|
|
165
|
+
const result: Record<string, unknown> = {};
|
|
166
|
+
Object.keys(value).forEach(key => {
|
|
167
|
+
result[key] = replaceFileRefs((value as Record<string, unknown>)[key], fileMap);
|
|
168
|
+
});
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
};
|
|
216
173
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
174
|
+
const normalizeResponse = (value: unknown): unknown => {
|
|
175
|
+
if (value && typeof value === 'object' && 'state' in value && 'body' in value) {
|
|
176
|
+
const typed = value as { state?: string; body?: unknown };
|
|
177
|
+
if (typed.state === 'error') {
|
|
178
|
+
const message = typeof typed.body === 'object' && typed.body && 'message' in typed.body
|
|
179
|
+
? String((typed.body as { message?: unknown }).message)
|
|
180
|
+
: 'TopicProtocol error';
|
|
181
|
+
throw new Error(message);
|
|
225
182
|
}
|
|
226
|
-
return
|
|
183
|
+
return typed.body;
|
|
227
184
|
}
|
|
185
|
+
return value;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// ── createHandler (module-level export, same as SymbolIntentApiServiceProxy) ─
|
|
189
|
+
|
|
190
|
+
export const createTopicHandler = <T extends object>(apiService: ApiService): ProxyHandler<T> => {
|
|
191
|
+
return {
|
|
192
|
+
get(target: T, prop: string | symbol, receiver: unknown): unknown {
|
|
193
|
+
if (prop === '_SimpleBoot_origin') return target;
|
|
194
|
+
|
|
195
|
+
const simConfig = getSim(target);
|
|
196
|
+
const value = Reflect.get(target, prop, receiver);
|
|
197
|
+
|
|
198
|
+
if (typeof value === 'function' && simConfig?.symbol) {
|
|
199
|
+
return function (...args: unknown[]) {
|
|
200
|
+
const f = value as Function;
|
|
201
|
+
|
|
202
|
+
const p = (userConfig?: TopicProtocolConfig) => {
|
|
203
|
+
const symbolValue = Array.isArray(simConfig.symbol) ? simConfig.symbol[0] : simConfig.symbol;
|
|
204
|
+
const symbolName = (symbolValue as Symbol | undefined)?.description ?? String(symbolValue ?? simConfig.symbol);
|
|
205
|
+
const targetPath = `Symbol.for(${symbolName}):/${String(prop)}`;
|
|
206
|
+
|
|
207
|
+
const { request, fileEntries } = buildRequest(targetPath, userConfig?.body, userConfig?.type ?? 'intent');
|
|
208
|
+
|
|
209
|
+
const formData = new FormData();
|
|
210
|
+
formData.append('body', JSON.stringify(request));
|
|
211
|
+
fileEntries.forEach(entry => {
|
|
212
|
+
if (entry.filename) formData.append(entry.id, entry.file, entry.filename);
|
|
213
|
+
else formData.append(entry.id, entry.file);
|
|
214
|
+
});
|
|
228
215
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
responseTransform: userConfig?.bypassTransform
|
|
262
|
-
? 'response'
|
|
263
|
-
: userConfig?.transformText
|
|
264
|
-
? 'text'
|
|
265
|
-
: async (response: Response) => {
|
|
266
|
-
const contentType = response.headers.get('Content-Type') ?? '';
|
|
267
|
-
if (contentType.toLowerCase().includes('multipart/mixed')) {
|
|
268
|
-
const parsed = await TopicProtocolApiServiceProxy.parseMultipartMixed(response);
|
|
269
|
-
if (!parsed) {
|
|
270
|
-
return undefined;
|
|
216
|
+
const headers: Record<string, string> = {
|
|
217
|
+
...(userConfig?.headers as Record<string, string> ?? {}),
|
|
218
|
+
[HttpHeaders.XSimpleBootHttpTopicProtocol]: 'topic',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const endpoint = userConfig?.endpoint ?? '/';
|
|
222
|
+
|
|
223
|
+
return (apiService as any).post({
|
|
224
|
+
target: endpoint,
|
|
225
|
+
config: {
|
|
226
|
+
responseTransform: userConfig?.bypassTransform
|
|
227
|
+
? 'response'
|
|
228
|
+
: userConfig?.transformText
|
|
229
|
+
? 'text'
|
|
230
|
+
: async (response: Response) => {
|
|
231
|
+
const ct = response.headers.get('Content-Type') ?? '';
|
|
232
|
+
if (ct.toLowerCase().includes('multipart/mixed')) {
|
|
233
|
+
const parsed = await parseMultipartMixed(response);
|
|
234
|
+
if (!parsed) return undefined;
|
|
235
|
+
|
|
236
|
+
const fileMap = new Map<string, { name?: string; mime?: string; size: number; buffer: ArrayBuffer }>();
|
|
237
|
+
let jsonBody: unknown = undefined;
|
|
238
|
+
|
|
239
|
+
parsed.parts.forEach(part => {
|
|
240
|
+
const partType = part.headers['content-type'] ?? '';
|
|
241
|
+
const disposition = part.headers['content-disposition'];
|
|
242
|
+
const name = parseDispositionName(disposition);
|
|
243
|
+
|
|
244
|
+
if (partType.includes('application/json')) {
|
|
245
|
+
const text = new TextDecoder().decode(part.body);
|
|
246
|
+
jsonBody = text ? JSON.parse(text) : undefined;
|
|
247
|
+
return;
|
|
271
248
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
const filename = TopicProtocolApiServiceProxy.parseDispositionFilename(disposition);
|
|
284
|
-
if (name) {
|
|
285
|
-
const view = part.body;
|
|
286
|
-
const safe = new Uint8Array(view.byteLength);
|
|
287
|
-
safe.set(view);
|
|
288
|
-
fileMap.set(name, {
|
|
289
|
-
name: filename,
|
|
290
|
-
mime: partType || 'application/octet-stream',
|
|
291
|
-
size: view.byteLength,
|
|
292
|
-
buffer: safe.buffer
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
if (jsonBody) {
|
|
297
|
-
const replaced = TopicProtocolApiServiceProxy.replaceFileRefs(jsonBody, fileMap);
|
|
298
|
-
return TopicProtocolApiServiceProxy.normalizeResponse(replaced);
|
|
249
|
+
if (name) {
|
|
250
|
+
const filename = parseDispositionFilename(disposition);
|
|
251
|
+
const safe = new Uint8Array(part.body.byteLength);
|
|
252
|
+
safe.set(part.body);
|
|
253
|
+
fileMap.set(name, {
|
|
254
|
+
name: filename,
|
|
255
|
+
mime: partType || 'application/octet-stream',
|
|
256
|
+
size: part.body.byteLength,
|
|
257
|
+
buffer: safe.buffer,
|
|
258
|
+
});
|
|
299
259
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (jsonBody) return normalizeResponse(replaceFileRefs(jsonBody, fileMap));
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const text = await response.text();
|
|
267
|
+
const parsed = text ? JSON.parse(text) : undefined;
|
|
268
|
+
return normalizeResponse(parsed);
|
|
269
|
+
},
|
|
270
|
+
transformText: userConfig?.transformText,
|
|
271
|
+
config: userConfig?.config,
|
|
272
|
+
fetch: {
|
|
273
|
+
credentials: 'include',
|
|
274
|
+
headers,
|
|
275
|
+
body: formData,
|
|
276
|
+
},
|
|
277
|
+
} as any,
|
|
278
|
+
});
|
|
318
279
|
};
|
|
319
|
-
|
|
320
|
-
|
|
280
|
+
|
|
281
|
+
args.push(p);
|
|
282
|
+
return Reflect.apply(f, target, args);
|
|
283
|
+
};
|
|
321
284
|
}
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
285
|
|
|
325
|
-
|
|
326
|
-
|
|
286
|
+
return Reflect.get(target, prop, receiver);
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
apply(target: T, thisArg: unknown, argArray: unknown[]): unknown {
|
|
290
|
+
console.log('TopicProtocolApiServiceProxy apply');
|
|
291
|
+
return Reflect.apply(target as any, thisArg, argArray as any[]);
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// ── Default factory export (same shape as SymbolIntentApiServiceProxy) ───────
|
|
297
|
+
|
|
298
|
+
export default (config?: SimConfig): ConstructorType<any> => {
|
|
299
|
+
@sim(config)
|
|
300
|
+
class TopicProtocolApiServiceProxy<T extends object> implements ProxyHandler<T> {
|
|
301
|
+
constructor(private apiService: ApiService) {}
|
|
302
|
+
|
|
303
|
+
get(target: T, prop: string | symbol, receiver: unknown): unknown {
|
|
304
|
+
return createTopicHandler<T>(this.apiService).get!(target, prop, receiver);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
apply(target: T, thisArg: unknown, argArray: unknown[]): unknown {
|
|
308
|
+
return createTopicHandler<T>(this.apiService).apply!(target, thisArg, argArray as any[]);
|
|
309
|
+
}
|
|
327
310
|
}
|
|
328
|
-
|
|
311
|
+
|
|
312
|
+
return TopicProtocolApiServiceProxy;
|
|
313
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {RandomUtils} from '@dooboostore/core';
|
|
2
2
|
import {TopicRequest, TopicResponse, TopicServerEventRequest, TopicServerEventResponse, TopicServerEventUnSubscribeRequest, TopicSubscribeRequest, TopicType, TopicUnsubscribeRequest} from './WebSocketManager';
|
|
3
|
-
import {
|
|
3
|
+
import {Observable, Subject} from '@dooboostore/core';
|
|
4
|
+
import { filter } from '@dooboostore/core/message/operators';
|
|
4
5
|
|
|
5
6
|
export * from '@dooboostore/core';
|
|
6
7
|
|
|
@@ -565,7 +566,7 @@ export class WebSocketClient {
|
|
|
565
566
|
const value: TopicRequest = { type: option?.type, uuid, requestUUID, body: body };
|
|
566
567
|
self.sendMessage(value);
|
|
567
568
|
}
|
|
568
|
-
return topicSubject.pipe(
|
|
569
|
+
return topicSubject.pipe(filter(data => data && data.requestUUID === requestUUID));
|
|
569
570
|
},
|
|
570
571
|
onUnsubscribe: (callback: OnUnsubscribeCallback) => {
|
|
571
572
|
onUnsubscribes.push(callback);
|
package/README.MD
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# @dooboostore/simple-boot-http-server
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@dooboostore/simple-boot-http-server)
|
|
4
|
-
[](https://github.com/dooboostore-develop/packages/actions/workflows/main.yaml)
|
|
5
|
-
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
|
|
7
|
-
**Full Documentation:** [https://dooboostore-develop.github.io/@dooboostore/simple-boot-http-server](https://dooboostore-develop.github.io/@dooboostore/simple-boot-http-server)
|
|
8
|
-
|
|
9
|
-
A lightweight and powerful HTTP web server framework for Node.js, built on `@dooboostore/simple-boot`.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Features
|
|
14
|
-
|
|
15
|
-
- **Declarative Routing**: Use decorators like `@Router`, `@Route`, `@GET`, `@POST`, etc., to map URL paths and HTTP methods directly to your controller methods.
|
|
16
|
-
- **Middleware via Filters**: Intercept the request-response cycle with `Filter` classes to implement cross-cutting concerns like logging, authentication, and CORS.
|
|
17
|
-
- **Lifecycle Endpoints**: Hook into the request lifecycle with `EndPoint` classes that run on request start, close, or error.
|
|
18
|
-
- **Integrated Exception Handling**: Leverage `@simple-boot`'s AOP capabilities to create robust global error handlers.
|
|
19
|
-
- **Built-in Session Management**: Easily manage user sessions with a built-in session manager and customizable storage.
|
|
20
|
-
- **HTTPS and HTTP/2 Support**: Secure your server with HTTPS and HTTP/2 configuration.
|
|
21
|
-
- **WebSocket Topics + Events**: Topic-based subscribe/request/response with server events.
|
|
22
|
-
- **WebSocket Binary File Transfer**: Send/receive files inside regular message payloads (JSON + binary framing).
|
|
23
|
-
- **WebSocket Auto Reconnect**: Automatic reconnect and re-subscribe with configurable retry.
|
|
24
|
-
|
|
25
|
-
## Quick Start
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npx @dooboostore/create-simple-boot-http-server my-server
|
|
29
|
-
cd my-server
|
|
30
|
-
npm start
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Security Hardening
|
|
34
|
-
|
|
35
|
-
- Enforce TLS in production: configure HTTPS (or terminate TLS at a trusted reverse proxy) and redirect HTTP to HTTPS.
|
|
36
|
-
- Keep session cookies hardened: use `httpOnly`, `secure`, `sameSite`, short expiration, and explicit `domain/path` when required.
|
|
37
|
-
- Protect WebSocket endpoints: validate origin, authenticate at connect/subscribe time, and authorize per topic.
|
|
38
|
-
- Validate all inputs: apply strict validation for route params, query/body payloads, and uploaded files (type/size/path).
|
|
39
|
-
- Avoid sensitive data leakage: never return stack traces, internal file paths, raw SQL errors, or secrets in responses/logs.
|
|
40
|
-
- Configure CORS minimally: allow only required origins/methods/headers, avoid wildcard credentials.
|
|
41
|
-
- Add rate limiting and timeout guards: mitigate brute force and resource-exhaustion attacks.
|
|
42
|
-
- Rotate secrets/keys regularly and load them from environment or a secret manager (never hardcode in source).
|
|
43
|
-
- Keep dependencies updated and scan regularly for CVEs.
|
|
44
|
-
|
|
45
|
-
### Minimal Production Checklist
|
|
46
|
-
|
|
47
|
-
- `HTTPS` enabled end-to-end
|
|
48
|
-
- Session cookie options set (`httpOnly`, `secure`, `sameSite`)
|
|
49
|
-
- CORS restricted to trusted origins
|
|
50
|
-
- Request/WS auth + authorization enforced
|
|
51
|
-
- Upload size/type limits configured
|
|
52
|
-
- Error responses sanitized
|
|
53
|
-
|
|
54
|
-
## Learn More
|
|
55
|
-
|
|
56
|
-
The detailed API documentation, including all decorators and usage examples, is available on our documentation website.
|
|
57
|
-
|
|
58
|
-
- WebSocket Guide: [document/websocket/README.md](document/websocket/README.md)
|
|
59
|
-
|
|
60
|
-
## License
|
|
61
|
-
|
|
62
|
-
This package is licensed under the [MIT License](https://opensource.org/licenses/MIT).
|