@cocoar/signalarrr 4.0.0-beta.15
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/dist/index.cjs +375 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +333 -0
- package/dist/index.js.map +1 -0
- package/docs/authorization.md +182 -0
- package/docs/client-api.md +358 -0
- package/docs/getting-started.md +300 -0
- package/docs/migration-v4.md +228 -0
- package/docs/proxy-generation.md +157 -0
- package/docs/server-api.md +251 -0
- package/docs/streaming.md +215 -0
- package/package.json +43 -0
- package/scripts/copy-assets.cjs +34 -0
- package/scripts/postinstall.cjs +42 -0
- package/skills/signalarrr/SKILL.md +154 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// src/harrr-connection.ts
|
|
2
|
+
import * as signalR from "@microsoft/signalr";
|
|
3
|
+
|
|
4
|
+
// src/models/cancellation-token-reference.ts
|
|
5
|
+
function isCancellationTokenReference(v) {
|
|
6
|
+
return typeof v === "object" && v !== null && typeof v["Id"] === "string";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/models/stream-reference.ts
|
|
10
|
+
function isStreamReference(v) {
|
|
11
|
+
if (typeof v !== "object" || v === null) return false;
|
|
12
|
+
const obj = v;
|
|
13
|
+
return typeof obj["Uri"] === "string" && Object.keys(obj).length === 1;
|
|
14
|
+
}
|
|
15
|
+
async function resolveStreamReference(ref) {
|
|
16
|
+
const response = await fetchStreamReference(ref);
|
|
17
|
+
return response.arrayBuffer();
|
|
18
|
+
}
|
|
19
|
+
async function resolveStreamReferenceAsStream(ref) {
|
|
20
|
+
const response = await fetchStreamReference(ref);
|
|
21
|
+
if (!response.body) {
|
|
22
|
+
throw new Error("StreamReference: response has no body stream");
|
|
23
|
+
}
|
|
24
|
+
return response.body;
|
|
25
|
+
}
|
|
26
|
+
async function fetchStreamReference(ref) {
|
|
27
|
+
const url = ref.Uri;
|
|
28
|
+
const scheme = url.split(":")[0]?.toLowerCase();
|
|
29
|
+
if (scheme !== "http" && scheme !== "https") {
|
|
30
|
+
throw new Error(`StreamReference: unsupported URI scheme '${scheme}'`);
|
|
31
|
+
}
|
|
32
|
+
const response = await fetch(url);
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
throw new Error(`StreamReference: download failed (${response.status} ${response.statusText})`);
|
|
35
|
+
}
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/models/harrr-error.ts
|
|
40
|
+
function parseHARRRError(error) {
|
|
41
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
42
|
+
const marker = "HARRRException: ";
|
|
43
|
+
const markerIndex = msg.indexOf(marker);
|
|
44
|
+
const jsonCandidate = markerIndex >= 0 ? msg.substring(markerIndex + marker.length) : msg;
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(jsonCandidate);
|
|
47
|
+
if (typeof parsed === "object" && parsed !== null && typeof parsed.Type === "string" && typeof parsed.Message === "string") {
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
const matches = /\[([\w.]+)\]\s*(.*)/m.exec(msg);
|
|
53
|
+
if (matches) {
|
|
54
|
+
return {
|
|
55
|
+
Type: matches[1] ?? "Error",
|
|
56
|
+
Message: matches[2] ?? msg
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
Type: "Error",
|
|
61
|
+
Message: msg
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/cancellation-manager.ts
|
|
66
|
+
var CancellationManager = class {
|
|
67
|
+
_controllers = /* @__PURE__ */ new Map();
|
|
68
|
+
create(id) {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
this._controllers.set(id, controller);
|
|
71
|
+
return controller.signal;
|
|
72
|
+
}
|
|
73
|
+
cancel(id) {
|
|
74
|
+
const controller = this._controllers.get(id);
|
|
75
|
+
if (controller) {
|
|
76
|
+
controller.abort();
|
|
77
|
+
this._controllers.delete(id);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
remove(id) {
|
|
81
|
+
this._controllers.delete(id);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/harrr-connection.ts
|
|
86
|
+
var HARRRConnection = class _HARRRConnection {
|
|
87
|
+
_hubConnection;
|
|
88
|
+
_accessTokenFactory = () => "";
|
|
89
|
+
_serverRequestHandlers = /* @__PURE__ */ new Map();
|
|
90
|
+
_serverStreamHandlers = /* @__PURE__ */ new Map();
|
|
91
|
+
_cancellationManager = new CancellationManager();
|
|
92
|
+
get baseUrl() {
|
|
93
|
+
return this._hubConnection.baseUrl;
|
|
94
|
+
}
|
|
95
|
+
set baseUrl(value) {
|
|
96
|
+
this._hubConnection.baseUrl = value;
|
|
97
|
+
}
|
|
98
|
+
get connectionId() {
|
|
99
|
+
return this._hubConnection.connectionId;
|
|
100
|
+
}
|
|
101
|
+
get state() {
|
|
102
|
+
return this._hubConnection.state;
|
|
103
|
+
}
|
|
104
|
+
get serverTimeoutInMilliseconds() {
|
|
105
|
+
return this._hubConnection.serverTimeoutInMilliseconds;
|
|
106
|
+
}
|
|
107
|
+
set serverTimeoutInMilliseconds(value) {
|
|
108
|
+
this._hubConnection.serverTimeoutInMilliseconds = value;
|
|
109
|
+
}
|
|
110
|
+
get keepAliveIntervalInMilliseconds() {
|
|
111
|
+
return this._hubConnection.keepAliveIntervalInMilliseconds;
|
|
112
|
+
}
|
|
113
|
+
set keepAliveIntervalInMilliseconds(value) {
|
|
114
|
+
this._hubConnection.keepAliveIntervalInMilliseconds = value;
|
|
115
|
+
}
|
|
116
|
+
constructor(hubConnection, _options) {
|
|
117
|
+
this._hubConnection = hubConnection;
|
|
118
|
+
const conn = hubConnection["connection"];
|
|
119
|
+
const factory = conn?.["_options"]?.["accessTokenFactory"] ?? conn?.["_accessTokenFactory"];
|
|
120
|
+
if (typeof factory === "function") {
|
|
121
|
+
this._accessTokenFactory = factory;
|
|
122
|
+
}
|
|
123
|
+
this._hubConnection.on("ChallengeAuthentication", (req) => {
|
|
124
|
+
return this._accessTokenFactory();
|
|
125
|
+
});
|
|
126
|
+
this._hubConnection.on("InvokeServerRequest", async (req) => {
|
|
127
|
+
if (req.StreamId) {
|
|
128
|
+
await this._streamBackToServer(req, req.StreamId);
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
const result = await this._dispatchServerMethod(req);
|
|
132
|
+
if (result instanceof Blob || result instanceof ArrayBuffer || isNodeBuffer(result)) {
|
|
133
|
+
return await this._uploadAndReturnReference(result);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
});
|
|
137
|
+
this._hubConnection.on("InvokeServerMessage", async (req) => {
|
|
138
|
+
try {
|
|
139
|
+
if (req.StreamId) {
|
|
140
|
+
await this._streamBackToServer(req, req.StreamId);
|
|
141
|
+
} else {
|
|
142
|
+
await this._dispatchServerMethod(req);
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
this._hubConnection.on("CancelTokenFromServer", (req) => {
|
|
148
|
+
if (req.CancellationGuid) {
|
|
149
|
+
this._cancellationManager.cancel(req.CancellationGuid);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async _dispatchServerMethod(req) {
|
|
154
|
+
const handler = this._serverRequestHandlers.get(req.Method);
|
|
155
|
+
if (!handler) return void 0;
|
|
156
|
+
const args = await this._prepareArgs(req);
|
|
157
|
+
return await handler(...args);
|
|
158
|
+
}
|
|
159
|
+
async _prepareArgs(req) {
|
|
160
|
+
const args = [];
|
|
161
|
+
for (const arg of req.Arguments ?? []) {
|
|
162
|
+
if (isCancellationTokenReference(arg) && req.CancellationGuid) {
|
|
163
|
+
args.push(this._cancellationManager.create(req.CancellationGuid));
|
|
164
|
+
} else if (isStreamReference(arg)) {
|
|
165
|
+
args.push(await resolveStreamReference(arg));
|
|
166
|
+
} else {
|
|
167
|
+
args.push(arg);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return args;
|
|
171
|
+
}
|
|
172
|
+
async _streamBackToServer(req, streamId) {
|
|
173
|
+
const args = await this._prepareArgs(req);
|
|
174
|
+
try {
|
|
175
|
+
const streamHandler = this._serverStreamHandlers.get(req.Method);
|
|
176
|
+
if (streamHandler) {
|
|
177
|
+
const stream = streamHandler(...args);
|
|
178
|
+
for await (const item of stream) {
|
|
179
|
+
await this._hubConnection.send("StreamItemToServer", streamId, item);
|
|
180
|
+
}
|
|
181
|
+
await this._hubConnection.send("StreamCompleteToServer", streamId, null);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const handler = this._serverRequestHandlers.get(req.Method);
|
|
185
|
+
if (handler) {
|
|
186
|
+
const result = await handler(...args);
|
|
187
|
+
if (result != null) {
|
|
188
|
+
if (typeof result === "object" && Symbol.asyncIterator in result) {
|
|
189
|
+
for await (const item of result) {
|
|
190
|
+
await this._hubConnection.send("StreamItemToServer", streamId, item);
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
await this._hubConnection.send("StreamItemToServer", streamId, result);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
await this._hubConnection.send("StreamCompleteToServer", streamId, null);
|
|
197
|
+
} else {
|
|
198
|
+
await this._hubConnection.send("StreamCompleteToServer", streamId, null);
|
|
199
|
+
}
|
|
200
|
+
} catch (err) {
|
|
201
|
+
await this._hubConnection.send("StreamCompleteToServer", streamId, String(err));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async _uploadAndReturnReference(data) {
|
|
205
|
+
const uploadUrl = await this._hubConnection.invoke("RequestUploadSlot");
|
|
206
|
+
let body;
|
|
207
|
+
if (data instanceof Blob) {
|
|
208
|
+
body = data;
|
|
209
|
+
} else if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
|
210
|
+
body = data;
|
|
211
|
+
} else {
|
|
212
|
+
body = String(data);
|
|
213
|
+
}
|
|
214
|
+
await fetch(uploadUrl, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
217
|
+
body
|
|
218
|
+
});
|
|
219
|
+
return { Uri: uploadUrl };
|
|
220
|
+
}
|
|
221
|
+
/** Prepare outgoing arguments — upload binary data and replace with StreamReferences. */
|
|
222
|
+
async _prepareOutgoingArgs(args) {
|
|
223
|
+
let hasStream = false;
|
|
224
|
+
for (const arg of args) {
|
|
225
|
+
if (arg instanceof Blob || arg instanceof ArrayBuffer || isNodeBuffer(arg)) {
|
|
226
|
+
hasStream = true;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!hasStream) return args;
|
|
231
|
+
const result = [];
|
|
232
|
+
for (const arg of args) {
|
|
233
|
+
if (arg instanceof Blob || arg instanceof ArrayBuffer || isNodeBuffer(arg)) {
|
|
234
|
+
result.push(await this._uploadAndReturnReference(arg));
|
|
235
|
+
} else {
|
|
236
|
+
result.push(arg);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
start() {
|
|
242
|
+
return this._hubConnection.start();
|
|
243
|
+
}
|
|
244
|
+
stop() {
|
|
245
|
+
return this._hubConnection.stop();
|
|
246
|
+
}
|
|
247
|
+
onClose(callback) {
|
|
248
|
+
this._hubConnection.onclose(callback);
|
|
249
|
+
}
|
|
250
|
+
onReconnecting(callback) {
|
|
251
|
+
this._hubConnection.onreconnecting(callback);
|
|
252
|
+
}
|
|
253
|
+
onReconnected(callback) {
|
|
254
|
+
this._hubConnection.onreconnected(callback);
|
|
255
|
+
}
|
|
256
|
+
async invoke(methodName, ...args) {
|
|
257
|
+
const preparedArgs = await this._prepareOutgoingArgs(args);
|
|
258
|
+
const msg = {
|
|
259
|
+
Method: methodName,
|
|
260
|
+
Arguments: preparedArgs,
|
|
261
|
+
Authorization: this._accessTokenFactory()
|
|
262
|
+
};
|
|
263
|
+
return this._hubConnection.invoke("InvokeMessageResult", msg).catch((err) => Promise.reject(this._extractException(err)));
|
|
264
|
+
}
|
|
265
|
+
async send(methodName, ...args) {
|
|
266
|
+
const preparedArgs = await this._prepareOutgoingArgs(args);
|
|
267
|
+
const msg = {
|
|
268
|
+
Method: methodName,
|
|
269
|
+
Arguments: preparedArgs,
|
|
270
|
+
Authorization: this._accessTokenFactory()
|
|
271
|
+
};
|
|
272
|
+
return this._hubConnection.send("SendMessage", msg);
|
|
273
|
+
}
|
|
274
|
+
stream(methodName, ...args) {
|
|
275
|
+
const msg = {
|
|
276
|
+
Method: methodName,
|
|
277
|
+
Arguments: args,
|
|
278
|
+
Authorization: this._accessTokenFactory()
|
|
279
|
+
};
|
|
280
|
+
return this._hubConnection.stream("StreamMessage", msg);
|
|
281
|
+
}
|
|
282
|
+
on(methodName, newMethod) {
|
|
283
|
+
this._hubConnection.on(methodName, newMethod);
|
|
284
|
+
}
|
|
285
|
+
/** Register a handler for server-to-client method calls (single return value). */
|
|
286
|
+
onServerMethod(methodName, func) {
|
|
287
|
+
this._serverRequestHandlers.set(methodName, func);
|
|
288
|
+
return this;
|
|
289
|
+
}
|
|
290
|
+
/** Register a handler for server-to-client streaming calls (returns AsyncIterable). */
|
|
291
|
+
onServerStreamMethod(methodName, func) {
|
|
292
|
+
this._serverStreamHandlers.set(methodName, func);
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
295
|
+
off(methodName, method) {
|
|
296
|
+
if (!method) {
|
|
297
|
+
this._hubConnection.off(methodName);
|
|
298
|
+
} else {
|
|
299
|
+
this._hubConnection.off(methodName, method);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
asSignalRHubConnection() {
|
|
303
|
+
return this._hubConnection;
|
|
304
|
+
}
|
|
305
|
+
static create(hubConnection, options) {
|
|
306
|
+
if (hubConnection instanceof Function) {
|
|
307
|
+
const builder = new signalR.HubConnectionBuilder();
|
|
308
|
+
hubConnection(builder);
|
|
309
|
+
return new _HARRRConnection(builder.build(), options);
|
|
310
|
+
}
|
|
311
|
+
return new _HARRRConnection(hubConnection, options);
|
|
312
|
+
}
|
|
313
|
+
_extractException(error) {
|
|
314
|
+
const parsed = parseHARRRError(error);
|
|
315
|
+
return { type: parsed.Type, message: parsed.Message };
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
function isNodeBuffer(value) {
|
|
319
|
+
return typeof globalThis !== "undefined" && typeof globalThis["Buffer"] === "function" && typeof globalThis["Buffer"]["isBuffer"] === "function" && globalThis["Buffer"].isBuffer(value);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/harrr-connection-options.ts
|
|
323
|
+
var HARRRConnectionOptions = class {
|
|
324
|
+
};
|
|
325
|
+
export {
|
|
326
|
+
HARRRConnection,
|
|
327
|
+
HARRRConnectionOptions,
|
|
328
|
+
isStreamReference,
|
|
329
|
+
parseHARRRError,
|
|
330
|
+
resolveStreamReference,
|
|
331
|
+
resolveStreamReferenceAsStream
|
|
332
|
+
};
|
|
333
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/harrr-connection.ts","../src/models/cancellation-token-reference.ts","../src/models/stream-reference.ts","../src/models/harrr-error.ts","../src/cancellation-manager.ts","../src/harrr-connection-options.ts"],"sourcesContent":["import * as signalR from '@microsoft/signalr';\nimport { ClientRequestMessage } from './models/client-request-message.js';\nimport { ServerRequestMessage } from './models/server-request-message.js';\nimport { isCancellationTokenReference } from './models/cancellation-token-reference.js';\nimport { isStreamReference, resolveStreamReference } from './models/stream-reference.js';\nimport { parseHARRRError } from './models/harrr-error.js';\nimport { HARRRConnectionOptions } from './harrr-connection-options.js';\nimport { CancellationManager } from './cancellation-manager.js';\n\nexport class HARRRConnection {\n private _hubConnection: signalR.HubConnection;\n private _accessTokenFactory: () => string = () => '';\n private _serverRequestHandlers = new Map<string, (...args: unknown[]) => unknown>();\n private _serverStreamHandlers = new Map<string, (...args: unknown[]) => AsyncIterable<unknown>>();\n private _cancellationManager = new CancellationManager();\n\n public get baseUrl(): string {\n return this._hubConnection.baseUrl;\n }\n\n public set baseUrl(value: string) {\n this._hubConnection.baseUrl = value;\n }\n\n public get connectionId(): string | null {\n return this._hubConnection.connectionId;\n }\n\n public get state(): signalR.HubConnectionState {\n return this._hubConnection.state;\n }\n\n public get serverTimeoutInMilliseconds(): number {\n return this._hubConnection.serverTimeoutInMilliseconds;\n }\n\n public set serverTimeoutInMilliseconds(value: number) {\n this._hubConnection.serverTimeoutInMilliseconds = value;\n }\n\n public get keepAliveIntervalInMilliseconds(): number {\n return this._hubConnection.keepAliveIntervalInMilliseconds;\n }\n\n public set keepAliveIntervalInMilliseconds(value: number) {\n this._hubConnection.keepAliveIntervalInMilliseconds = value;\n }\n\n constructor(hubConnection: signalR.HubConnection, _options?: HARRRConnectionOptions) {\n this._hubConnection = hubConnection;\n\n const conn = (hubConnection as unknown as Record<string, unknown>)['connection'] as Record<string, unknown> | undefined;\n const factory =\n (conn?.['_options'] as Record<string, unknown> | undefined)?.['accessTokenFactory'] ??\n conn?.['_accessTokenFactory'];\n if (typeof factory === 'function') {\n this._accessTokenFactory = factory as () => string;\n }\n\n // Native client results — return values are sent back to the server automatically by SignalR\n this._hubConnection.on('ChallengeAuthentication', (req: ServerRequestMessage) => {\n return this._accessTokenFactory();\n });\n\n this._hubConnection.on('InvokeServerRequest', async (req: ServerRequestMessage) => {\n // If StreamId is present, stream results back instead of returning a single value\n if (req.StreamId) {\n await this._streamBackToServer(req, req.StreamId);\n return undefined;\n }\n const result = await this._dispatchServerMethod(req);\n\n // If the result is binary data (Blob, ArrayBuffer, Buffer), upload via HTTP\n // and return a StreamReference instead\n if (result instanceof Blob || result instanceof ArrayBuffer ||\n isNodeBuffer(result)) {\n return await this._uploadAndReturnReference(result);\n }\n\n return result;\n });\n\n this._hubConnection.on('InvokeServerMessage', async (req: ServerRequestMessage) => {\n try {\n if (req.StreamId) {\n await this._streamBackToServer(req, req.StreamId);\n } else {\n await this._dispatchServerMethod(req);\n }\n } catch {\n // ignored — fire-and-forget\n }\n });\n\n this._hubConnection.on('CancelTokenFromServer', (req: ServerRequestMessage) => {\n if (req.CancellationGuid) {\n this._cancellationManager.cancel(req.CancellationGuid);\n }\n });\n }\n\n private async _dispatchServerMethod(req: ServerRequestMessage): Promise<unknown> {\n const handler = this._serverRequestHandlers.get(req.Method);\n if (!handler) return undefined;\n\n const args = await this._prepareArgs(req);\n return await handler(...args);\n }\n\n private async _prepareArgs(req: ServerRequestMessage): Promise<unknown[]> {\n const args: unknown[] = [];\n for (const arg of req.Arguments ?? []) {\n if (isCancellationTokenReference(arg) && req.CancellationGuid) {\n args.push(this._cancellationManager.create(req.CancellationGuid));\n } else if (isStreamReference(arg)) {\n // Download the stream data via HTTP and pass as ArrayBuffer\n args.push(await resolveStreamReference(arg));\n } else {\n args.push(arg);\n }\n }\n return args;\n }\n\n private async _streamBackToServer(req: ServerRequestMessage, streamId: string): Promise<void> {\n const args = await this._prepareArgs(req);\n\n try {\n // Try stream handler first\n const streamHandler = this._serverStreamHandlers.get(req.Method);\n if (streamHandler) {\n const stream = streamHandler(...args);\n for await (const item of stream) {\n await this._hubConnection.send('StreamItemToServer', streamId, item);\n }\n await this._hubConnection.send('StreamCompleteToServer', streamId, null);\n return;\n }\n\n // Fall back to regular handler — send single result as one item\n const handler = this._serverRequestHandlers.get(req.Method);\n if (handler) {\n const result = await handler(...args);\n if (result != null) {\n // Check if result is async iterable\n if (typeof result === 'object' && Symbol.asyncIterator in (result as object)) {\n for await (const item of result as AsyncIterable<unknown>) {\n await this._hubConnection.send('StreamItemToServer', streamId, item);\n }\n } else {\n await this._hubConnection.send('StreamItemToServer', streamId, result);\n }\n }\n await this._hubConnection.send('StreamCompleteToServer', streamId, null);\n } else {\n await this._hubConnection.send('StreamCompleteToServer', streamId, null);\n }\n } catch (err) {\n await this._hubConnection.send('StreamCompleteToServer', streamId, String(err));\n }\n }\n\n private async _uploadAndReturnReference(data: Blob | ArrayBuffer | Uint8Array | unknown): Promise<{ Uri: string }> {\n // Request an upload URL from the server\n const uploadUrl = await this._hubConnection.invoke<string>('RequestUploadSlot');\n\n // Upload the data via HTTP POST\n let body: BodyInit;\n if (data instanceof Blob) {\n body = data;\n } else if (data instanceof ArrayBuffer || data instanceof Uint8Array) {\n body = data as BodyInit;\n } else {\n body = String(data);\n }\n await fetch(uploadUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/octet-stream' },\n body,\n });\n\n return { Uri: uploadUrl };\n }\n\n /** Prepare outgoing arguments — upload binary data and replace with StreamReferences. */\n private async _prepareOutgoingArgs(args: unknown[]): Promise<unknown[]> {\n let hasStream = false;\n for (const arg of args) {\n if (arg instanceof Blob || arg instanceof ArrayBuffer ||\n isNodeBuffer(arg)) {\n hasStream = true;\n break;\n }\n }\n if (!hasStream) return args;\n\n const result: unknown[] = [];\n for (const arg of args) {\n if (arg instanceof Blob || arg instanceof ArrayBuffer ||\n isNodeBuffer(arg)) {\n result.push(await this._uploadAndReturnReference(arg));\n } else {\n result.push(arg);\n }\n }\n return result;\n }\n\n public start(): Promise<void> {\n return this._hubConnection.start();\n }\n\n public stop(): Promise<void> {\n return this._hubConnection.stop();\n }\n\n public onClose(callback: (error?: Error) => void): void {\n this._hubConnection.onclose(callback);\n }\n\n public onReconnecting(callback: (error?: Error) => void): void {\n this._hubConnection.onreconnecting(callback);\n }\n\n public onReconnected(callback: (connectionId?: string) => void): void {\n this._hubConnection.onreconnected(callback);\n }\n\n public async invoke<T>(methodName: string, ...args: unknown[]): Promise<T> {\n const preparedArgs = await this._prepareOutgoingArgs(args);\n const msg: ClientRequestMessage = {\n Method: methodName,\n Arguments: preparedArgs,\n Authorization: this._accessTokenFactory(),\n };\n return this._hubConnection\n .invoke<T>('InvokeMessageResult', msg)\n .catch(err => Promise.reject(this._extractException(err)));\n }\n\n public async send(methodName: string, ...args: unknown[]): Promise<void> {\n const preparedArgs = await this._prepareOutgoingArgs(args);\n const msg: ClientRequestMessage = {\n Method: methodName,\n Arguments: preparedArgs,\n Authorization: this._accessTokenFactory(),\n };\n return this._hubConnection.send('SendMessage', msg);\n }\n\n public stream<T>(methodName: string, ...args: unknown[]): signalR.IStreamResult<T> {\n const msg: ClientRequestMessage = {\n Method: methodName,\n Arguments: args,\n Authorization: this._accessTokenFactory(),\n };\n return this._hubConnection.stream<T>('StreamMessage', msg);\n }\n\n public on(methodName: string, newMethod: (...args: any[]) => void): void {\n this._hubConnection.on(methodName, newMethod);\n }\n\n /** Register a handler for server-to-client method calls (single return value). */\n public onServerMethod(methodName: string, func: (...args: unknown[]) => unknown): this {\n this._serverRequestHandlers.set(methodName, func);\n return this;\n }\n\n /** Register a handler for server-to-client streaming calls (returns AsyncIterable). */\n public onServerStreamMethod(methodName: string, func: (...args: unknown[]) => AsyncIterable<unknown>): this {\n this._serverStreamHandlers.set(methodName, func);\n return this;\n }\n\n public off(methodName: string): void;\n public off(methodName: string, method: (...args: any[]) => void): void;\n public off(methodName: string, method?: (...args: any[]) => void): void {\n if (!method) {\n this._hubConnection.off(methodName);\n } else {\n this._hubConnection.off(methodName, method);\n }\n }\n\n public asSignalRHubConnection(): signalR.HubConnection {\n return this._hubConnection;\n }\n\n public static create(\n hubConnection: signalR.HubConnection | ((builder: signalR.HubConnectionBuilder) => void),\n options?: HARRRConnectionOptions,\n ): HARRRConnection {\n if (hubConnection instanceof Function) {\n const builder = new signalR.HubConnectionBuilder();\n hubConnection(builder);\n return new HARRRConnection(builder.build(), options);\n }\n return new HARRRConnection(hubConnection, options);\n }\n\n private _extractException(error: unknown): { type: string; message: string } {\n const parsed = parseHARRRError(error);\n return { type: parsed.Type, message: parsed.Message };\n }\n}\n\n/** Runtime check for Node.js Buffer without importing @types/node */\nfunction isNodeBuffer(value: unknown): boolean {\n return typeof globalThis !== 'undefined' &&\n typeof (globalThis as Record<string, unknown>)['Buffer'] === 'function' &&\n typeof ((globalThis as Record<string, unknown>)['Buffer'] as Record<string, unknown>)['isBuffer'] === 'function' &&\n ((globalThis as Record<string, unknown>)['Buffer'] as { isBuffer: (v: unknown) => boolean }).isBuffer(value);\n}\n","export interface CancellationTokenReference {\n Id: string;\n}\n\nexport function isCancellationTokenReference(v: unknown): v is CancellationTokenReference {\n return typeof v === 'object' && v !== null && typeof (v as Record<string, unknown>)['Id'] === 'string';\n}\n","export interface StreamReference {\n Uri: string;\n}\n\nexport function isStreamReference(v: unknown): v is StreamReference {\n if (typeof v !== 'object' || v === null) return false;\n const obj = v as Record<string, unknown>;\n return typeof obj['Uri'] === 'string' && Object.keys(obj).length === 1;\n}\n\n/** Resolve a StreamReference by downloading the data — returns the full content buffered in memory. */\nexport async function resolveStreamReference(ref: StreamReference): Promise<ArrayBuffer> {\n const response = await fetchStreamReference(ref);\n return response.arrayBuffer();\n}\n\n/** Resolve a StreamReference as a ReadableStream — for large files, avoids buffering in memory. */\nexport async function resolveStreamReferenceAsStream(ref: StreamReference): Promise<ReadableStream<Uint8Array>> {\n const response = await fetchStreamReference(ref);\n if (!response.body) {\n throw new Error('StreamReference: response has no body stream');\n }\n return response.body;\n}\n\nasync function fetchStreamReference(ref: StreamReference): Promise<Response> {\n const url = ref.Uri;\n const scheme = url.split(':')[0]?.toLowerCase();\n if (scheme !== 'http' && scheme !== 'https') {\n throw new Error(`StreamReference: unsupported URI scheme '${scheme}'`);\n }\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`StreamReference: download failed (${response.status} ${response.statusText})`);\n }\n return response;\n}\n","/**\n * Structured error envelope from SignalARRR server exceptions.\n * The server serializes exceptions as JSON in the HubException message string.\n */\nexport interface HARRRError {\n Type: string;\n Message: string;\n StackTrace?: string;\n}\n\n/**\n * Parse a HubException message into a structured HARRRError.\n * Supports both the new JSON format and the legacy `[Type] Message` format.\n *\n * SignalR wraps HubException messages with prefix text like:\n * \"An unexpected error occurred invoking '...' on the server. HARRRException: {json}\"\n */\nexport function parseHARRRError(error: unknown): HARRRError {\n const msg = error instanceof Error ? error.message : String(error);\n\n // Extract JSON after \"HARRRException: \" marker (SignalR wrapping)\n const marker = 'HARRRException: ';\n const markerIndex = msg.indexOf(marker);\n const jsonCandidate = markerIndex >= 0 ? msg.substring(markerIndex + marker.length) : msg;\n\n // Try JSON format\n try {\n const parsed = JSON.parse(jsonCandidate);\n if (typeof parsed === 'object' && parsed !== null && typeof parsed.Type === 'string' && typeof parsed.Message === 'string') {\n return parsed as HARRRError;\n }\n } catch {\n // Not JSON — try legacy format\n }\n\n // Legacy format: [Type] Message\n const matches = /\\[([\\w.]+)\\]\\s*(.*)/m.exec(msg);\n if (matches) {\n return {\n Type: matches[1] ?? 'Error',\n Message: matches[2] ?? msg,\n };\n }\n\n // Fallback\n return {\n Type: 'Error',\n Message: msg,\n };\n}\n","export class CancellationManager {\n private _controllers = new Map<string, AbortController>();\n\n create(id: string): AbortSignal {\n const controller = new AbortController();\n this._controllers.set(id, controller);\n return controller.signal;\n }\n\n cancel(id: string): void {\n const controller = this._controllers.get(id);\n if (controller) {\n controller.abort();\n this._controllers.delete(id);\n }\n }\n\n remove(id: string): void {\n this._controllers.delete(id);\n }\n}\n","export class HARRRConnectionOptions {}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Authorization
|
|
2
|
+
|
|
3
|
+
SignalARRR integrates with ASP.NET Core's authorization system. Use standard
|
|
4
|
+
`[Authorize]` and `[AllowAnonymous]` attributes on hub methods and classes.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
### Server configuration
|
|
11
|
+
|
|
12
|
+
```csharp
|
|
13
|
+
var builder = WebApplication.CreateBuilder(args);
|
|
14
|
+
|
|
15
|
+
builder.Services.AddAuthentication("Bearer")
|
|
16
|
+
.AddJwtBearer(options => {
|
|
17
|
+
options.Authority = "https://auth.example.com";
|
|
18
|
+
options.Audience = "my-api";
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
builder.Services.AddAuthorization(options => {
|
|
22
|
+
options.AddPolicy("AdminOnly", policy =>
|
|
23
|
+
policy.RequireRole("admin"));
|
|
24
|
+
options.AddPolicy("Premium", policy =>
|
|
25
|
+
policy.RequireClaim("subscription", "premium"));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
builder.Services.AddSignalR();
|
|
29
|
+
builder.Services.AddSignalARRR(options => options
|
|
30
|
+
.AddServerMethodsFrom(typeof(Program).Assembly));
|
|
31
|
+
|
|
32
|
+
var app = builder.Build();
|
|
33
|
+
|
|
34
|
+
app.UseRouting();
|
|
35
|
+
app.UseAuthentication(); // Must come before UseAuthorization
|
|
36
|
+
app.UseAuthorization();
|
|
37
|
+
app.MapHARRRController<ChatHub>("/chathub");
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Client token configuration
|
|
41
|
+
|
|
42
|
+
```csharp
|
|
43
|
+
var connection = HARRRConnection.Create(builder => {
|
|
44
|
+
builder.WithUrl("https://localhost:5001/chathub", options => {
|
|
45
|
+
options.AccessTokenProvider = () => Task.FromResult(GetJwtToken());
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Method-Level Authorization
|
|
53
|
+
|
|
54
|
+
Apply `[Authorize]` to individual methods in ServerMethods classes:
|
|
55
|
+
|
|
56
|
+
```csharp
|
|
57
|
+
public class AdminMethods : ServerMethods<ChatHub> {
|
|
58
|
+
|
|
59
|
+
[Authorize("AdminOnly")]
|
|
60
|
+
public Task DeleteUser(int userId) {
|
|
61
|
+
// Only accessible to users with "admin" role
|
|
62
|
+
return _userService.Delete(userId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
[Authorize("Premium")]
|
|
66
|
+
public Task<List<string>> GetPremiumContent() {
|
|
67
|
+
return _contentService.GetPremium();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
[AllowAnonymous]
|
|
71
|
+
public Task<string> GetPublicInfo() {
|
|
72
|
+
// Accessible to everyone, even without authentication
|
|
73
|
+
return Task.FromResult("Public info");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Class-Level Authorization
|
|
81
|
+
|
|
82
|
+
Apply `[Authorize]` to the ServerMethods class — all methods inherit it:
|
|
83
|
+
|
|
84
|
+
```csharp
|
|
85
|
+
[Authorize]
|
|
86
|
+
public class SecureMethods : ServerMethods<ChatHub> {
|
|
87
|
+
|
|
88
|
+
// All methods require authentication
|
|
89
|
+
public Task<string> GetSecret() => Task.FromResult("secret");
|
|
90
|
+
|
|
91
|
+
[AllowAnonymous]
|
|
92
|
+
public Task<string> GetPublic() => Task.FromResult("public");
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Hub-Level Authorization Inheritance
|
|
99
|
+
|
|
100
|
+
If the Hub class has `[Authorize]`, all ServerMethods classes for that hub
|
|
101
|
+
inherit the authorization requirement automatically:
|
|
102
|
+
|
|
103
|
+
```csharp
|
|
104
|
+
[Authorize] // All methods on all ServerMethods<SecureHub> require auth
|
|
105
|
+
public class SecureHub : HARRR {
|
|
106
|
+
public SecureHub(IServiceProvider sp) : base(sp) { }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// This class inherits [Authorize] from SecureHub
|
|
110
|
+
public class SecureHubMethods : ServerMethods<SecureHub> {
|
|
111
|
+
|
|
112
|
+
// Requires authentication (inherited from hub)
|
|
113
|
+
public Task<string> GetData() => Task.FromResult("data");
|
|
114
|
+
|
|
115
|
+
// Override: allow anonymous access despite hub-level auth
|
|
116
|
+
[AllowAnonymous]
|
|
117
|
+
public Task<string> GetPublicData() => Task.FromResult("public");
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Inheritance priority
|
|
122
|
+
|
|
123
|
+
Authorization attributes are resolved in this order:
|
|
124
|
+
|
|
125
|
+
1. **Method-level** — `[Authorize]` or `[AllowAnonymous]` on the method itself
|
|
126
|
+
2. **Class-level** — `[Authorize]` on the `ServerMethods<T>` class
|
|
127
|
+
3. **Hub-level** — `[Authorize]` on the `HARRR` hub class (via `ServerMethods<T>` generic argument)
|
|
128
|
+
|
|
129
|
+
If no authorization attributes are found at any level, the method is accessible
|
|
130
|
+
anonymously.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Authorization flow
|
|
135
|
+
|
|
136
|
+
When a client calls an authorized method:
|
|
137
|
+
|
|
138
|
+
1. SignalARRR checks for `[AllowAnonymous]` — if present, allows access
|
|
139
|
+
2. Checks for `[Authorize]` on method → class → hub
|
|
140
|
+
3. If authorization data exists:
|
|
141
|
+
a. Validates the token (from `AccessTokenProvider`) against configured schemes
|
|
142
|
+
b. Caches the authentication result until `ClientContext.UserValidUntil`
|
|
143
|
+
c. Evaluates the authorization policy
|
|
144
|
+
4. If authentication has expired, sends a challenge to the client
|
|
145
|
+
5. Client responds with a fresh token via the challenge protocol
|
|
146
|
+
6. Server re-evaluates with the new token
|
|
147
|
+
|
|
148
|
+
### Token caching
|
|
149
|
+
|
|
150
|
+
SignalARRR caches successful authentication results. It only re-authenticates
|
|
151
|
+
when `ClientContext.UserValidUntil` expires. This avoids re-validating the token
|
|
152
|
+
on every RPC call.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Accessing the authenticated user
|
|
157
|
+
|
|
158
|
+
```csharp
|
|
159
|
+
public class UserMethods : ServerMethods<ChatHub> {
|
|
160
|
+
public Task<string> WhoAmI() {
|
|
161
|
+
// From ClientContext (enhanced)
|
|
162
|
+
var user = ClientContext.User;
|
|
163
|
+
var name = user.Identity?.Name ?? "Anonymous";
|
|
164
|
+
|
|
165
|
+
// Or from standard SignalR context
|
|
166
|
+
var signalRUser = Context.User;
|
|
167
|
+
|
|
168
|
+
return Task.FromResult(name);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## SignalR negotiate-level auth
|
|
176
|
+
|
|
177
|
+
Note that `[Authorize]` on the Hub class also blocks the SignalR negotiate
|
|
178
|
+
endpoint (HTTP level). Unauthenticated clients will get a 401 response when
|
|
179
|
+
trying to connect — they won't even establish a SignalR connection.
|
|
180
|
+
|
|
181
|
+
This is different from method-level auth, where the connection succeeds but
|
|
182
|
+
individual method calls are rejected.
|