@serverkgg/bridge 0.0.7 → 0.0.9
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/package.json +4 -3
- package/src/cli/validate.ts +1 -0
- package/src/context/bridgeContext.ts +10 -0
- package/src/context/index.ts +1 -0
- package/src/core/bridgeDownloadError.ts +10 -0
- package/src/core/bridgeHold.ts +12 -0
- package/src/core/bridgeKind.ts +2 -0
- package/src/core/bridgeUserError.test.ts +23 -0
- package/src/core/bridgeUserError.ts +12 -0
- package/src/core/index.ts +3 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +85 -0
- package/src/manifest/manifest.ts +21 -0
- package/src/manifest/presence.ts +48 -0
- package/src/manifest/primitives.ts +6 -0
- package/src/modules/bridge.ts +45 -2
- package/src/modules/bridgeDriver.ts +2 -0
- package/src/modules/bridgeModule.ts +25 -3
- package/src/panel/bridgeDetail.ts +49 -0
- package/src/panel/bridgeLayout.ts +1 -0
- package/src/panel/bridgePanel.ts +30 -0
- package/src/panel/bridgeUploadMode.ts +4 -0
- package/src/panel/index.ts +2 -0
- package/src/protocol/bridgeEgress.ts +8 -1
- package/src/protocol/bridgeEvents.ts +4 -0
- package/src/protocol/bridgeFrame.ts +28 -1
- package/src/protocol/bridgeLimits.ts +10 -0
- package/src/runtime/bridgeActions.ts +7 -0
- package/src/runtime/bridgeDriver.test.ts +477 -2
- package/src/runtime/bridgeDriver.ts +116 -3
- package/src/runtime/bridgeEgress.ts +3 -0
- package/src/runtime/bridgeEvents.ts +7 -5
- package/src/runtime/bridgeFiles.test.ts +625 -15
- package/src/runtime/bridgeFiles.ts +152 -45
- package/src/runtime/bridgeInstall.test.ts +118 -0
- package/src/runtime/bridgeInstall.ts +55 -0
- package/src/runtime/bridgeNet.ts +32 -3
- package/src/runtime/bridgePending.test.ts +148 -0
- package/src/runtime/bridgePending.ts +43 -0
- package/src/runtime/bridgePrivilege.test.ts +77 -0
- package/src/runtime/bridgePrivilege.ts +5 -3
- package/src/runtime/bridgeRegistry.test.ts +134 -0
- package/src/runtime/bridgeRegistry.ts +26 -4
- package/src/runtime/bridgeServer.test.ts +338 -0
- package/src/runtime/bridgeServer.ts +25 -3
- package/src/runtime/bridgeSupervisor.ts +18 -0
- package/src/runtime/bridgeZip.ts +179 -58
- package/src/runtime/serve.ts +64 -4
- package/src/wire/index.ts +2 -0
- package/src/wire/panel.test.ts +451 -0
- package/src/wire/panel.ts +295 -0
- package/src/wire/zEnvKey.ts +7 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { BRIDGE_PENDING_FILES_LIMIT, type BridgePendingFile } from "../protocol";
|
|
3
|
+
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
4
|
+
import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
|
|
5
|
+
|
|
6
|
+
const file = (id: string): BridgePendingFile => ({
|
|
7
|
+
id,
|
|
8
|
+
title: {
|
|
9
|
+
ar: id,
|
|
10
|
+
en: id,
|
|
11
|
+
},
|
|
12
|
+
fileName: `${id}.jar`,
|
|
13
|
+
sizeBytes: null,
|
|
14
|
+
digest: null,
|
|
15
|
+
sourceUrl: null,
|
|
16
|
+
note: null,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const files = (count: number) => {
|
|
20
|
+
return Array.from({
|
|
21
|
+
length: count,
|
|
22
|
+
}).map((_entry, index) => file(`file-${index}`));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const recorder = () => {
|
|
26
|
+
const warnings: string[] = [];
|
|
27
|
+
|
|
28
|
+
const onLog: BridgeLeveledLogger = Object.assign(
|
|
29
|
+
() => {
|
|
30
|
+
return;
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
warn: (message: string) => {
|
|
34
|
+
warnings.push(message);
|
|
35
|
+
},
|
|
36
|
+
error: () => {
|
|
37
|
+
return;
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
onLog,
|
|
44
|
+
warnings,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe("unsupportedPending", () => {
|
|
49
|
+
test("answers the sentinel a driver without a pending module gets", () => {
|
|
50
|
+
expect(unsupportedPending()).toEqual({
|
|
51
|
+
supported: false,
|
|
52
|
+
staging: "",
|
|
53
|
+
extensions: [],
|
|
54
|
+
files: [],
|
|
55
|
+
rejected: null,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("pendingResult", () => {
|
|
61
|
+
test("carries the staging path, the extensions and the files the driver listed", () => {
|
|
62
|
+
const { onLog, warnings } = recorder();
|
|
63
|
+
|
|
64
|
+
expect(
|
|
65
|
+
pendingResult(
|
|
66
|
+
".serverk-staging/pending-upload",
|
|
67
|
+
[
|
|
68
|
+
"jar",
|
|
69
|
+
],
|
|
70
|
+
files(2),
|
|
71
|
+
onLog,
|
|
72
|
+
),
|
|
73
|
+
).toEqual({
|
|
74
|
+
supported: true,
|
|
75
|
+
staging: ".serverk-staging/pending-upload",
|
|
76
|
+
extensions: [
|
|
77
|
+
"jar",
|
|
78
|
+
],
|
|
79
|
+
files: files(2),
|
|
80
|
+
rejected: null,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(warnings).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("truncates a list longer than the platform takes and warns instead of throwing", () => {
|
|
87
|
+
const { onLog, warnings } = recorder();
|
|
88
|
+
|
|
89
|
+
const result = pendingResult(
|
|
90
|
+
".serverk-staging/pending-upload",
|
|
91
|
+
[
|
|
92
|
+
"jar",
|
|
93
|
+
],
|
|
94
|
+
files(BRIDGE_PENDING_FILES_LIMIT + 5),
|
|
95
|
+
onLog,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result.files).toHaveLength(BRIDGE_PENDING_FILES_LIMIT);
|
|
99
|
+
expect(result.files.at(0)?.id).toBe("file-0");
|
|
100
|
+
expect(warnings).toEqual([
|
|
101
|
+
"pending list truncated",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("rejectedPending", () => {
|
|
107
|
+
test("names every file still pending as what the upload was expected to be", () => {
|
|
108
|
+
const { onLog } = recorder();
|
|
109
|
+
|
|
110
|
+
const result = rejectedPending(
|
|
111
|
+
pendingResult(
|
|
112
|
+
".serverk-staging/pending-upload",
|
|
113
|
+
[
|
|
114
|
+
"jar",
|
|
115
|
+
],
|
|
116
|
+
files(2),
|
|
117
|
+
onLog,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(result.rejected).toEqual({
|
|
122
|
+
expected: [
|
|
123
|
+
"file-0.jar",
|
|
124
|
+
"file-1.jar",
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
expect(result.files).toHaveLength(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("leaves an empty expectation when the driver has nothing pending left", () => {
|
|
131
|
+
const { onLog } = recorder();
|
|
132
|
+
|
|
133
|
+
expect(
|
|
134
|
+
rejectedPending(
|
|
135
|
+
pendingResult(
|
|
136
|
+
".serverk-staging/pending-upload",
|
|
137
|
+
[
|
|
138
|
+
"jar",
|
|
139
|
+
],
|
|
140
|
+
[],
|
|
141
|
+
onLog,
|
|
142
|
+
),
|
|
143
|
+
).rejected,
|
|
144
|
+
).toEqual({
|
|
145
|
+
expected: [],
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BRIDGE_PENDING_FILES_LIMIT, type BridgePendingFile, type BridgePendingResult } from "../protocol";
|
|
2
|
+
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
3
|
+
|
|
4
|
+
export const unsupportedPending = (): BridgePendingResult => {
|
|
5
|
+
return {
|
|
6
|
+
supported: false,
|
|
7
|
+
staging: "",
|
|
8
|
+
extensions: [],
|
|
9
|
+
files: [],
|
|
10
|
+
rejected: null,
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const pendingResult = (
|
|
15
|
+
staging: string,
|
|
16
|
+
extensions: string[],
|
|
17
|
+
files: BridgePendingFile[],
|
|
18
|
+
onLog: BridgeLeveledLogger,
|
|
19
|
+
): BridgePendingResult => {
|
|
20
|
+
if (files.length > BRIDGE_PENDING_FILES_LIMIT) {
|
|
21
|
+
onLog.warn("pending list truncated", {
|
|
22
|
+
files: files.length,
|
|
23
|
+
limit: BRIDGE_PENDING_FILES_LIMIT,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
supported: true,
|
|
29
|
+
staging,
|
|
30
|
+
extensions,
|
|
31
|
+
files: files.slice(0, BRIDGE_PENDING_FILES_LIMIT),
|
|
32
|
+
rejected: null,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const rejectedPending = (result: BridgePendingResult): BridgePendingResult => {
|
|
37
|
+
return {
|
|
38
|
+
...result,
|
|
39
|
+
rejected: {
|
|
40
|
+
expected: result.files.map((file) => file.fileName),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, spyOn } from "bun:test";
|
|
2
|
+
import { privilegePlan, readEffectiveUid, setprivArgv, spawnCredentials } from "./bridgePrivilege";
|
|
3
|
+
|
|
4
|
+
const COMMAND = [
|
|
5
|
+
"java",
|
|
6
|
+
"-jar",
|
|
7
|
+
"server.jar",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
describe("spawnCredentials", () => {
|
|
11
|
+
it("carries the uid and gid for the spawn options", () => {
|
|
12
|
+
expect(spawnCredentials(1000, 1000)).toEqual({
|
|
13
|
+
uid: 1000,
|
|
14
|
+
gid: 1000,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("setprivArgv", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
spyOn(Bun, "which").mockRestore();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("wraps the command with a full privilege drop", () => {
|
|
25
|
+
spyOn(Bun, "which").mockReturnValue("/usr/bin/setpriv");
|
|
26
|
+
|
|
27
|
+
expect(setprivArgv(COMMAND, 1000, 1000)).toEqual([
|
|
28
|
+
"setpriv",
|
|
29
|
+
"--reuid=1000",
|
|
30
|
+
"--regid=1000",
|
|
31
|
+
"--clear-groups",
|
|
32
|
+
"--",
|
|
33
|
+
...COMMAND,
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("refuses to build a wrapper the image cannot run", () => {
|
|
38
|
+
spyOn(Bun, "which").mockReturnValue(null);
|
|
39
|
+
|
|
40
|
+
expect(() => setprivArgv(COMMAND, 1000, 1000)).toThrow("setpriv is missing");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("privilegePlan", () => {
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
spyOn(Bun, "which").mockRestore();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("passes credentials to the runtime when the spawn uid option is honoured", () => {
|
|
50
|
+
const plan = privilegePlan(true, COMMAND, 1000, 1000);
|
|
51
|
+
|
|
52
|
+
expect(plan.argv, "a native runtime must run the bare command").toEqual(COMMAND);
|
|
53
|
+
expect(plan.credentials, "the drop must ride the spawn options or the game runs as root").toEqual({
|
|
54
|
+
uid: 1000,
|
|
55
|
+
gid: 1000,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("falls back to setpriv with no spawn credentials otherwise", () => {
|
|
60
|
+
spyOn(Bun, "which").mockReturnValue("/usr/bin/setpriv");
|
|
61
|
+
|
|
62
|
+
const plan = privilegePlan(false, COMMAND, 1000, 1000);
|
|
63
|
+
|
|
64
|
+
expect(plan.argv.at(0), "the wrapper must lead the argv").toBe("setpriv");
|
|
65
|
+
expect(plan.credentials, "credentials on a setpriv wrapper would fail the spawn").toEqual({});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("readEffectiveUid", () => {
|
|
70
|
+
it("reads this process's own effective uid", async () => {
|
|
71
|
+
expect(await readEffectiveUid(process.pid)).toBe(process.getuid?.() ?? 0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("answers null for a pid that does not exist", async () => {
|
|
75
|
+
expect(await readEffectiveUid(2 ** 22 - 1)).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -91,11 +91,13 @@ export const setprivArgv = (command: string[], uid: number, gid: number) => {
|
|
|
91
91
|
];
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
-
export const
|
|
95
|
-
const native = await probeNativeUid(uid, gid);
|
|
96
|
-
|
|
94
|
+
export const privilegePlan = (native: boolean, command: string[], uid: number, gid: number) => {
|
|
97
95
|
return {
|
|
98
96
|
argv: native ? command : setprivArgv(command, uid, gid),
|
|
99
97
|
credentials: native ? spawnCredentials(uid, gid) : {},
|
|
100
98
|
};
|
|
101
99
|
};
|
|
100
|
+
|
|
101
|
+
export const unprivilegedArgv = async (command: string[], uid: number, gid: number) => {
|
|
102
|
+
return privilegePlan(await probeNativeUid(uid, gid), command, uid, gid);
|
|
103
|
+
};
|
|
@@ -426,3 +426,137 @@ describe("preview modules", () => {
|
|
|
426
426
|
);
|
|
427
427
|
});
|
|
428
428
|
});
|
|
429
|
+
|
|
430
|
+
describe("detail modules", () => {
|
|
431
|
+
test("lists read and its own action ids in the manifest", () => {
|
|
432
|
+
const registry = registryFor({
|
|
433
|
+
...baseDriver(),
|
|
434
|
+
modules: {
|
|
435
|
+
modpackStatus: {
|
|
436
|
+
kind: BridgeKind.Detail,
|
|
437
|
+
read: async () => null,
|
|
438
|
+
actions: {
|
|
439
|
+
update: async () => null,
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
expect(registry.manifest().modules).toEqual([
|
|
446
|
+
{
|
|
447
|
+
id: "modpackStatus",
|
|
448
|
+
kind: BridgeKind.Detail,
|
|
449
|
+
requiresRunning: false,
|
|
450
|
+
refreshSeconds: null,
|
|
451
|
+
pageSize: null,
|
|
452
|
+
dependsOn: [],
|
|
453
|
+
actions: [
|
|
454
|
+
BridgeAction.Read,
|
|
455
|
+
"update",
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
]);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("returns what read answers", async () => {
|
|
462
|
+
const registry = registryFor({
|
|
463
|
+
...baseDriver(),
|
|
464
|
+
modules: {
|
|
465
|
+
modpackStatus: {
|
|
466
|
+
kind: BridgeKind.Detail,
|
|
467
|
+
read: async () => ({
|
|
468
|
+
id: "modrinth:abc",
|
|
469
|
+
title: "Better MC",
|
|
470
|
+
subtitle: "1.21.1 · NeoForge",
|
|
471
|
+
description: null,
|
|
472
|
+
image: null,
|
|
473
|
+
badges: [],
|
|
474
|
+
stats: [],
|
|
475
|
+
links: [],
|
|
476
|
+
stale: false,
|
|
477
|
+
actions: [],
|
|
478
|
+
}),
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
expect(await registry.invoke("modpackStatus", BridgeAction.Read)).toMatchObject({
|
|
484
|
+
id: "modrinth:abc",
|
|
485
|
+
title: "Better MC",
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("hands the argument bag to an action and returns its outcome", async () => {
|
|
490
|
+
const received: BridgeValues[] = [];
|
|
491
|
+
|
|
492
|
+
const registry = registryFor({
|
|
493
|
+
...baseDriver(),
|
|
494
|
+
modules: {
|
|
495
|
+
modpackStatus: {
|
|
496
|
+
kind: BridgeKind.Detail,
|
|
497
|
+
read: async () => null,
|
|
498
|
+
actions: {
|
|
499
|
+
update: async (_context, args) => {
|
|
500
|
+
received.push(args);
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
variables: {
|
|
504
|
+
MODPACK: "modrinth:abc:v2",
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
expect(
|
|
514
|
+
await registry.invoke("modpackStatus", "update", {
|
|
515
|
+
id: "modrinth:abc",
|
|
516
|
+
}),
|
|
517
|
+
).toEqual({
|
|
518
|
+
variables: {
|
|
519
|
+
MODPACK: "modrinth:abc:v2",
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
expect(received).toEqual([
|
|
524
|
+
{
|
|
525
|
+
id: "modrinth:abc",
|
|
526
|
+
},
|
|
527
|
+
]);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("refuses an action named after a read", () => {
|
|
531
|
+
expect(() =>
|
|
532
|
+
registryFor({
|
|
533
|
+
...baseDriver(),
|
|
534
|
+
modules: {
|
|
535
|
+
modpackStatus: {
|
|
536
|
+
kind: BridgeKind.Detail,
|
|
537
|
+
read: async () => null,
|
|
538
|
+
actions: {
|
|
539
|
+
read: async () => null,
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
}),
|
|
544
|
+
).toThrow('module "modpackStatus" declares action "read", which is reserved for reads');
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("refuses an action the module does not answer", async () => {
|
|
548
|
+
const registry = registryFor({
|
|
549
|
+
...baseDriver(),
|
|
550
|
+
modules: {
|
|
551
|
+
modpackStatus: {
|
|
552
|
+
kind: BridgeKind.Detail,
|
|
553
|
+
read: async () => null,
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
await expect(registry.invoke("modpackStatus", "update")).rejects.toThrow(
|
|
559
|
+
'module "modpackStatus" does not support "update"',
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
@@ -18,7 +18,11 @@ interface CacheEntry {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const requiresRunning = (module: BridgePanelModule) => {
|
|
21
|
-
if (
|
|
21
|
+
if (
|
|
22
|
+
module.kind === BridgeKind.Actions
|
|
23
|
+
|| module.kind === BridgeKind.Collection
|
|
24
|
+
|| module.kind === BridgeKind.Detail
|
|
25
|
+
) {
|
|
22
26
|
return module.requiresRunning ?? false;
|
|
23
27
|
}
|
|
24
28
|
|
|
@@ -26,7 +30,11 @@ const requiresRunning = (module: BridgePanelModule) => {
|
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
const refreshOf = (module: BridgePanelModule) => {
|
|
29
|
-
|
|
33
|
+
if (module.kind === BridgeKind.Collection || module.kind === BridgeKind.Detail) {
|
|
34
|
+
return module.refreshSeconds ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null;
|
|
30
38
|
};
|
|
31
39
|
|
|
32
40
|
const dependsOf = (module: BridgePanelModule) => {
|
|
@@ -138,11 +146,11 @@ export const createRegistry = (
|
|
|
138
146
|
const modules = driver.modules ?? {};
|
|
139
147
|
|
|
140
148
|
for (const [id, module] of Object.entries(modules)) {
|
|
141
|
-
if (module.kind !== BridgeKind.Actions) {
|
|
149
|
+
if (module.kind !== BridgeKind.Actions && module.kind !== BridgeKind.Detail) {
|
|
142
150
|
continue;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
|
-
for (const action of Object.keys(module.actions)) {
|
|
153
|
+
for (const action of Object.keys(module.actions ?? {})) {
|
|
146
154
|
if (isReadAction(action)) {
|
|
147
155
|
throw new BridgeDriverError(
|
|
148
156
|
`module "${id}" declares action "${action}", which is reserved for reads — pick another id`,
|
|
@@ -290,6 +298,20 @@ export const createRegistry = (
|
|
|
290
298
|
break;
|
|
291
299
|
}
|
|
292
300
|
|
|
301
|
+
case BridgeKind.Detail: {
|
|
302
|
+
if (action === BridgeAction.Read) {
|
|
303
|
+
return await module.read(context);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const detailAction = module.actions?.[action];
|
|
307
|
+
|
|
308
|
+
if (detailAction) {
|
|
309
|
+
return await detailAction(context, args);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
|
|
293
315
|
case BridgeKind.Catalog: {
|
|
294
316
|
if (action === BridgeAction.Search) {
|
|
295
317
|
return await module.search(context, {
|