Skip to content

Commit b3d6918

Browse files
os-warrenclaude
andauthored
test(client,runtime): close 337 refused authz-resolver reads — the false-green class on this lane's nine sites (#18082)
Fixes #18070 The false-green class, closed on the nine sites this lane owns: seven `@objectstack/client` fixtures where the refusals were **visible in the log**, and two `@objectstack/runtime` integration fixtures where the symptom was **pinned rather than closed**. Test-only: no product code is touched, and no dependency edge onto `plugin-auth` / `plugin-security` is added. ## What was wrong `core/src/security/resolve-authz-context.ts` reaches five `sys_*` tables through `tryFind`, which classifies a **missing table** as "not provisioned" and answers `[]`. A fixture that boots a real `ObjectQL` over a real `SqlDriver` but registers a narrower object set therefore gets a green it did not earn: the assertion passes because the read returned empty, not because the state was empty. A suite in that condition cannot turn red when grant resolution breaks. ## The card's counts, RE-MEASURED (not transcribed) Every number in the card was re-derived on `fb29f62ce`, classified by the `(table, filter, limit)` triple of the eight reads the resolver issues — so `sys_position ... where name in (...) limit 200` counts and `where name = ? limit 1` on the same table does not. | file | card | measured | after | |---|---|---|---| | `client/src/client.metadata-prefix.test.ts` | 95 | **95** | **0** | | `client/src/client.hono.test.ts` | 35 | **35** | **0** | | `client/src/client.data-prefix.test.ts` | 25 | **25** | **0** | | `client/src/client.batch-transaction.test.ts` | 25 | **25** | **0** | | `client/src/auth-get-session-envelope.test.ts` | 21 | **21** | **0** | | `client/src/client.environment-scoping.test.ts` | 20 | **20** | **0** | | `client/src/auth-login-register-envelope.test.ts` | 6 | **6** | **0** | | **sum** | **227** | **227** | **0** | Whole-package control, same tree, same command shape: `@objectstack/client` emitted **264** refused reads of which **227** were resolver-class; after, **37** of which **0** are. The 37 that remain are `sys_metadata` 25, `sys_setting` 7, `sys_metadata_history` 3, `sys_organization` 2 — the `probeInstallOrganizations` and boot-metadata classes, a different card. The seven summing to the whole-package 227 is what proves there is no eighth site. ## The instrument under-reads, and here is the proof The two `@objectstack/runtime` sites route their refusals through `captureExpectedReadRefusals` (#10629 / #11081), which **withholds the driver line**. Measured on `fb29f62ce`: `grep -c "refused a read on"` over a full run of either file reads a clean **0**, while the capture's own counter reads ``` notifications.hono.integration refusals {"sys_user":10,"sys_member":10,"sys_user_position":10, "sys_user_permission_set":10,"sys_position":10,"sys_setting":2} engineFrames identical → 52 total, 50 resolver-class notification-schema-conformance refusals {"sys_user":12,"sys_member":12,"sys_user_position":12, "sys_user_permission_set":12,"sys_position":12,"sys_setting":3} engineFrames identical → 63 total, 60 resolver-class ``` After: `{"sys_setting":2}` and `{"sys_setting":3}` — **110 resolver-class refusals closed**, and `sys_setting`, which is not resolver-class, deliberately left exactly as it was. Total across this PR: **337** refused authz-resolver reads closed (227 visible + 110 withheld). ## The count falls because the read SUCCEEDS Nothing is silenced, filtered or re-levelled. Proven positively by a one-off probe that seeded one row per table and printed what the read returns — injected, run, then restored under a `trap` and verified byte-exact with `git hash-object` against each path's HEAD blob: ``` [#18070 PROBE ROWS] sys_user [{"id":"probe-user",...,"email":"probe@example.com"}] [#18070 PROBE ROWS] sys_member [{"id":"probe-mem",...,"user_id":"probe-user","role":"admin"}] [#18070 PROBE ROWS] sys_user_position [{"id":"probe-up",...,"position":"everyone"}] [#18070 PROBE ROWS] sys_user_permission_set [{"id":"probe-ups",...,"permission_set_id":"probe-ps"}] [#18070 PROBE ROWS] sys_position [{"id":"probe-pos",...,"name":"everyone","active":true}] RESTORED packages/runtime/src/notifications.hono.integration.test.ts blob=9f7158bc9... == HEAD-BLOB-of-the-same-path RESTORED packages/runtime/src/notification-schema-conformance.integration.test.ts blob=299cd3b37... == HEAD-BLOB-of-the-same-path RESTORED packages/client/src/client.environment-scoping.test.ts blob=2aa383b08... == HEAD-BLOB-of-the-same-path RESTORE VERIFIED: git diff HEAD empty over all three probed paths ``` A by-product worth naming: seeding those rows made the new runtime assertion go RED (`expected [ { id: 'probe-user', …(6) } ] to deeply equal []`). That is the assertion reading real state rather than a stub. ## ⭐ The `runtime` half: a passing assertion had to move, deliberately This is the review's sticking point, and it should be. Both runtime fixtures declared `ABSENT_AUTHZ_TABLES` and asserted `noise.silentChannels(ALWAYS_READ_AUTHZ_TABLES)` — an assertion that each of the five reads was **still being refused**. That pins the symptom. Closing the read necessarily falsifies it, and leaving it in place would leave a pin asserting a number that no longer describes reality. It is **replaced, not deleted**. What it asserted about behaviour — "these five reads really happen on this path" — is now asserted in the direction the fix runs, by `expectResolverAuthzReadsSucceed()`: each read SUCCEEDS and answers `[]` because the state is empty rather than because the table is missing. Same call sites, same `-t`-safety (per authed test in the hono file; in `afterAll` for the conformance file, with the shutdown moved into a `finally` so the old invariant — a failure here can never leave the kernel running — survives the reordering the live-engine read forces). `ABSENT_AUTHZ_TABLES` shrinks to `['sys_setting']`, which is the shared capture module's own prescribed repair: *"a table that started resolving means the fixture now provisions it"*. ⭐ Shrinking that list is also what keeps the capture from becoming a mute: `captureDriver` forwards an **unrecognised** refusal straight to `console.warn`, so a regression that stops provisioning one of the five is now LOUD as well as red — where, while the five were declared, the same regression would have been withheld and merely counted. ## Ablation — the new pin can fail Registration deleted, on-disk landing proven by anchor counts before/after (`rt2=1 cl1=1` → `rt2=0 cl1=0`, one `ABLATED` marker each), restored under a `trap` and verified byte-exact against HEAD: | ablated | result | |---|---| | `notification-schema-conformance.integration.test.ts` | **Test Files 1 failed** — while all 8 `Tests` pass. The failure is the new `afterAll` assertion, i.e. the pin catches exactly the regression it exists for. The five driver refusal lines are also **visible in the output**, confirming the loudness claim above. | | `client.environment-scoping.test.ts` | refusals back to **20 resolver-class** — and the suite still **passes (5/5)**. That is the false green itself, reproduced on demand. | ## Scope and fences - `resolve-authz-context.ts` lives in `packages/core/src/security/` (`domain:engine`) and is **not touched**. Nothing needed it. - `packages/spec` is **not touched**. The only new dependency on it is a `import type { ServiceObject }` — a type already re-exported from `@objectstack/spec/data`, which both packages already depend on. - Objects are registered **locally**, with only the columns the reading path touches (`id` is not declared anywhere — the registry supplies the primary key). `sys_position_permission_set` and `sys_permission_set` are deliberately absent: the resolver reaches them only after a `sys_position` row resolves, and measurement confirms neither appears in any of these files' refusals before or after. - Changeset: **`skip-changeset`**, measured rather than assumed. Both packages publish `files: ["dist","README.md","CHANGELOG.md"]`; `grep -rl` for the new symbols (`AUTHZ_RESOLVER_OBJECTS`, `expectResolverAuthzReadsSucceed`) over every one of those paths returns **zero hits**, against a positive control (`ObjectStackClient`, `createRestApiPlugin`) that hits `dist/`. No published artefact moves. ## Acceptance notes Out of scope, observed while measuring, filed as nothing: - **The non-resolver refusal classes are still there and are deliberately untouched.** After this PR `@objectstack/client` still emits 37 refused reads (`sys_metadata` 25, `sys_setting` 7, `sys_metadata_history` 3, `sys_organization` 2) and the two `runtime` fixtures still withhold `sys_setting` (2 and 3). None is resolver-class by the `(table, filter, limit)` classifier — they are the `probeInstallOrganizations` / boot-metadata-load / localization-settings classes, which #18070's body and PR #18067's own notes already separate out. Noted, not filed: the carrier is whoever picks up that class, which lands in these same files. - **The shared capture has no affordance for the positive direction.** `expected-read-refusal-noise.ts` prescribes the repair for "a table that started resolving" (drop it from the declared list) but offers nothing to assert the success that replaces the refusal, so each consumer hand-rolls it — this PR writes `expectResolverAuthzReadsSucceed()` twice. A design observation about a test helper, not a defect in it, and not a reproducible failure: noted, not filed, carrier none. ## Verification See the report comment on #18070 for the full gate table and exit codes. Authored by Claude Code in session `session_01TbSMtGzMrtPwh925wDEZd5` — kept as prose because this body was EDITED after creation, and the edit channel appends its own footer block: a session-URL footer sent on an edit ends up with the platform's bare one beneath it, two footers where the form allows one. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 57343f7 commit b3d6918

9 files changed

Lines changed: 1047 additions & 99 deletions

‎packages/client/src/auth-get-session-envelope.test.ts‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { AuthManager } from '@objectstack/plugin-auth';
5353
import * as identityObjects from '@objectstack/platform-objects/identity';
5454
import { BaseResponseSchema, SessionResponseSchema, SessionSchema } from '@objectstack/spec/api';
5555
import { ObjectStackClient } from './index';
56+
import type { ServiceObject } from '@objectstack/spec/data';
5657

5758
const SECRET = 'test-secret-at-least-32-chars-long!!';
5859
const ORIGIN = 'http://localhost:3000';
@@ -76,6 +77,83 @@ const IDENTITY_OBJECTS = Object.values(
7677
typeof (o as { fields?: unknown }).fields === 'object',
7778
);
7879

80+
/**
81+
* ⚠️ [#18070] The authz objects every authenticated request in this file
82+
* makes core's `resolveUserAuthzGrants`
83+
* (`core/src/security/resolve-authz-context.ts`) read. They belong to
84+
* `@objectstack/plugin-security` and are spelled LOCALLY here, carrying only
85+
* the columns that reading path touches, so this suite adds no dependency edge
86+
* onto that package — the shape PR #17982 and PR #18067 landed for the same
87+
* defect.
88+
*
89+
* Without them the driver REFUSED every one of those reads. Measured on
90+
* `fb29f62ce`, classified by the `(table, filter, limit)` triple of the eight
91+
* reads the resolver issues: **21** resolver-class `refused a read on` driver
92+
* lines in this file, 7 per table across 3. `tryFind` classifies a missing
93+
* table as "not provisioned" and answers `[]`, so nothing went red: every
94+
* assertion below passed over grant reads that never happened — a green this
95+
* suite had not earned, and one it could not lose if grant resolution broke.
96+
*
97+
* ⛔ Registering the tables is what makes those reads SUCCEED. The count must
98+
* ⛔ not fall by silencing, filtering or re-levelling the driver line.
99+
*
100+
* ⭐ Only THREE tables, and that is the measurement talking: this fixture
101+
* already provisions `sys_user` and `sys_member` through `IDENTITY_OBJECTS`
102+
* above, so those two reads always succeeded here. The three below are the
103+
* `@objectstack/plugin-security` side, which nothing in this file provisioned.
104+
*
105+
* Columns, and why each is here — every other column of the real objects is
106+
* deliberately absent, because no read on this path touches it. `id` is not
107+
* declared anywhere below: the registry supplies the primary key itself.
108+
* `sys_user_position` user_id (filter), position, organization_id
109+
* `sys_user_permission_set` user_id (filter), permission_set_id, organization_id
110+
* `sys_position` name (filter), active (`isRowActive`),
111+
* organization_id (the driver's tenant scope)
112+
*
113+
* ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the
114+
* resolver reaches them only once a `sys_position` row resolves and a
115+
* permission-set id is collected, and nothing here seeds either — measured,
116+
* neither table appears in this file's refusals, before or after.
117+
*/
118+
const AUTHZ_RESOLVER_OBJECTS: { owner: string; def: ServiceObject }[] = [
119+
{
120+
owner: '@objectstack/plugin-security',
121+
def: {
122+
name: 'sys_user_position',
123+
label: 'User Position',
124+
fields: {
125+
user_id: { type: 'text', label: 'User' },
126+
position: { type: 'text', label: 'Position' },
127+
organization_id: { type: 'text', label: 'Organization' },
128+
},
129+
},
130+
},
131+
{
132+
owner: '@objectstack/plugin-security',
133+
def: {
134+
name: 'sys_user_permission_set',
135+
label: 'User Permission Set',
136+
fields: {
137+
user_id: { type: 'text', label: 'User' },
138+
permission_set_id: { type: 'text', label: 'Permission Set' },
139+
organization_id: { type: 'text', label: 'Organization' },
140+
},
141+
},
142+
},
143+
{
144+
owner: '@objectstack/plugin-security',
145+
def: {
146+
name: 'sys_position',
147+
label: 'Position',
148+
fields: {
149+
name: { type: 'text', label: 'Name' },
150+
active: { type: 'boolean', label: 'Active' },
151+
organization_id: { type: 'text', label: 'Organization' },
152+
},
153+
},
154+
},
155+
];
156+
79157
const engines: ObjectQL[] = [];
80158

81159
const makeEngine = async (): Promise<ObjectQL> => {
@@ -86,6 +164,11 @@ const makeEngine = async (): Promise<ObjectQL> => {
86164
for (const object of IDENTITY_OBJECTS) {
87165
engine.registry.registerObject(object as never, '@objectstack/plugin-auth');
88166
}
167+
// [#18070] The authz resolver's plugin-security-side reads, registered
168+
// before the sync so the driver PROVISIONS them rather than refusing them.
169+
for (const o of AUTHZ_RESOLVER_OBJECTS) {
170+
engine.registry.registerObject(o.def, o.owner);
171+
}
89172
await engine.syncSchemas();
90173
return engine;
91174
};

‎packages/client/src/auth-login-register-envelope.test.ts‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { AuthManager } from '@objectstack/plugin-auth';
5656
import * as identityObjects from '@objectstack/platform-objects/identity';
5757
import { BaseResponseSchema, SessionResponseSchema, SessionSchema } from '@objectstack/spec/api';
5858
import { ObjectStackClient } from './index';
59+
import type { ServiceObject } from '@objectstack/spec/data';
5960

6061
const SECRET = 'test-secret-at-least-32-chars-long!!';
6162
const ORIGIN = 'http://localhost:3000';
@@ -79,6 +80,83 @@ const IDENTITY_OBJECTS = Object.values(
7980
typeof (o as { fields?: unknown }).fields === 'object',
8081
);
8182

83+
/**
84+
* ⚠️ [#18070] The authz objects every authenticated request in this file
85+
* makes core's `resolveUserAuthzGrants`
86+
* (`core/src/security/resolve-authz-context.ts`) read. They belong to
87+
* `@objectstack/plugin-security` and are spelled LOCALLY here, carrying only
88+
* the columns that reading path touches, so this suite adds no dependency edge
89+
* onto that package — the shape PR #17982 and PR #18067 landed for the same
90+
* defect.
91+
*
92+
* Without them the driver REFUSED every one of those reads. Measured on
93+
* `fb29f62ce`, classified by the `(table, filter, limit)` triple of the eight
94+
* reads the resolver issues: **6** resolver-class `refused a read on` driver
95+
* lines in this file, 2 per table across 3. `tryFind` classifies a missing
96+
* table as "not provisioned" and answers `[]`, so nothing went red: every
97+
* assertion below passed over grant reads that never happened — a green this
98+
* suite had not earned, and one it could not lose if grant resolution broke.
99+
*
100+
* ⛔ Registering the tables is what makes those reads SUCCEED. The count must
101+
* ⛔ not fall by silencing, filtering or re-levelling the driver line.
102+
*
103+
* ⭐ Only THREE tables, and that is the measurement talking: this fixture
104+
* already provisions `sys_user` and `sys_member` through `IDENTITY_OBJECTS`
105+
* above, so those two reads always succeeded here. The three below are the
106+
* `@objectstack/plugin-security` side, which nothing in this file provisioned.
107+
*
108+
* Columns, and why each is here — every other column of the real objects is
109+
* deliberately absent, because no read on this path touches it. `id` is not
110+
* declared anywhere below: the registry supplies the primary key itself.
111+
* `sys_user_position` user_id (filter), position, organization_id
112+
* `sys_user_permission_set` user_id (filter), permission_set_id, organization_id
113+
* `sys_position` name (filter), active (`isRowActive`),
114+
* organization_id (the driver's tenant scope)
115+
*
116+
* ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the
117+
* resolver reaches them only once a `sys_position` row resolves and a
118+
* permission-set id is collected, and nothing here seeds either — measured,
119+
* neither table appears in this file's refusals, before or after.
120+
*/
121+
const AUTHZ_RESOLVER_OBJECTS: { owner: string; def: ServiceObject }[] = [
122+
{
123+
owner: '@objectstack/plugin-security',
124+
def: {
125+
name: 'sys_user_position',
126+
label: 'User Position',
127+
fields: {
128+
user_id: { type: 'text', label: 'User' },
129+
position: { type: 'text', label: 'Position' },
130+
organization_id: { type: 'text', label: 'Organization' },
131+
},
132+
},
133+
},
134+
{
135+
owner: '@objectstack/plugin-security',
136+
def: {
137+
name: 'sys_user_permission_set',
138+
label: 'User Permission Set',
139+
fields: {
140+
user_id: { type: 'text', label: 'User' },
141+
permission_set_id: { type: 'text', label: 'Permission Set' },
142+
organization_id: { type: 'text', label: 'Organization' },
143+
},
144+
},
145+
},
146+
{
147+
owner: '@objectstack/plugin-security',
148+
def: {
149+
name: 'sys_position',
150+
label: 'Position',
151+
fields: {
152+
name: { type: 'text', label: 'Name' },
153+
active: { type: 'boolean', label: 'Active' },
154+
organization_id: { type: 'text', label: 'Organization' },
155+
},
156+
},
157+
},
158+
];
159+
82160
const engines: ObjectQL[] = [];
83161

84162
const makeEngine = async (): Promise<ObjectQL> => {
@@ -89,6 +167,11 @@ const makeEngine = async (): Promise<ObjectQL> => {
89167
for (const object of IDENTITY_OBJECTS) {
90168
engine.registry.registerObject(object as never, '@objectstack/plugin-auth');
91169
}
170+
// [#18070] The authz resolver's plugin-security-side reads, registered
171+
// before the sync so the driver PROVISIONS them rather than refusing them.
172+
for (const o of AUTHZ_RESOLVER_OBJECTS) {
173+
engine.registry.registerObject(o.def, o.owner);
174+
}
92175
await engine.syncSchemas();
93176
return engine;
94177
};

‎packages/client/src/client.batch-transaction.test.ts‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,104 @@ import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
3434
import { createRestApiPlugin } from '@objectstack/runtime';
3535
import { ObjectStackClient } from './index';
3636
import type { IHttpServer } from '@objectstack/spec/contracts';
37+
import type { ServiceObject } from '@objectstack/spec/data';
38+
39+
/**
40+
* ⚠️ [#18070] The authz objects every authenticated request in this file
41+
* makes core's `resolveUserAuthzGrants`
42+
* (`core/src/security/resolve-authz-context.ts`) read. They belong to
43+
* `@objectstack/plugin-auth` / `@objectstack/plugin-security` and are spelled
44+
* LOCALLY here, carrying only the columns that reading path touches, so this
45+
* suite adds no dependency edge onto either package — the shape PR #17982 and
46+
* PR #18067 landed for the same defect.
47+
*
48+
* Without them the driver REFUSED every one of those reads. Measured on
49+
* `fb29f62ce`, classified by the `(table, filter, limit)` triple of the eight
50+
* reads the resolver issues: **25** resolver-class `refused a read on` driver
51+
* lines in this file, 5 per table across 5. `tryFind` classifies a missing
52+
* table as "not provisioned" and answers `[]`, so nothing went red: every
53+
* assertion below passed over grant reads that never happened — a green this
54+
* suite had not earned, and one it could not lose if grant resolution broke.
55+
*
56+
* ⛔ Registering the tables is what makes those reads SUCCEED. The count must
57+
* ⛔ not fall by silencing, filtering or re-levelling the driver line.
58+
*
59+
* Columns, and why each is here — every other column of the real objects is
60+
* deliberately absent, because no read on this path touches it. `id` is not
61+
* declared anywhere below: the registry supplies the primary key itself, and
62+
* it is what `sys_user`'s `id` filter reads.
63+
* `sys_user` email (the `current_user.email` owner-RLS fallback)
64+
* `sys_member` user_id / organization_id (both filters), role
65+
* `sys_user_position` user_id (filter), position, organization_id
66+
* `sys_user_permission_set` user_id (filter), permission_set_id, organization_id
67+
* `sys_position` name (filter), active (`isRowActive`),
68+
* organization_id (the driver's tenant scope)
69+
*
70+
* ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the
71+
* resolver reaches them only once a `sys_position` row resolves and a
72+
* permission-set id is collected, and nothing here seeds either — measured,
73+
* neither table appears in this file's refusals, before or after.
74+
*/
75+
const AUTHZ_RESOLVER_OBJECTS: { owner: string; def: ServiceObject }[] = [
76+
{
77+
owner: '@objectstack/plugin-auth',
78+
def: {
79+
name: 'sys_user',
80+
label: 'User',
81+
fields: {
82+
email: { type: 'text', label: 'Email' },
83+
},
84+
},
85+
},
86+
{
87+
owner: '@objectstack/plugin-auth',
88+
def: {
89+
name: 'sys_member',
90+
label: 'Member',
91+
fields: {
92+
user_id: { type: 'text', label: 'User' },
93+
organization_id: { type: 'text', label: 'Organization' },
94+
role: { type: 'text', label: 'Role' },
95+
},
96+
},
97+
},
98+
{
99+
owner: '@objectstack/plugin-security',
100+
def: {
101+
name: 'sys_user_position',
102+
label: 'User Position',
103+
fields: {
104+
user_id: { type: 'text', label: 'User' },
105+
position: { type: 'text', label: 'Position' },
106+
organization_id: { type: 'text', label: 'Organization' },
107+
},
108+
},
109+
},
110+
{
111+
owner: '@objectstack/plugin-security',
112+
def: {
113+
name: 'sys_user_permission_set',
114+
label: 'User Permission Set',
115+
fields: {
116+
user_id: { type: 'text', label: 'User' },
117+
permission_set_id: { type: 'text', label: 'Permission Set' },
118+
organization_id: { type: 'text', label: 'Organization' },
119+
},
120+
},
121+
},
122+
{
123+
owner: '@objectstack/plugin-security',
124+
def: {
125+
name: 'sys_position',
126+
label: 'Position',
127+
fields: {
128+
name: { type: 'text', label: 'Name' },
129+
active: { type: 'boolean', label: 'Active' },
130+
organization_id: { type: 'text', label: 'Organization' },
131+
},
132+
},
133+
},
134+
];
37135

38136
describe('data.batchTransaction (live Hono, #1604)', () => {
39137
let baseUrl: string;
@@ -106,6 +204,12 @@ describe('data.batchTransaction (live Hono, #1604)', () => {
106204
// write fails with `no such table`.
107205
await ql.syncObjectSchema('project');
108206
await ql.syncObjectSchema('task');
207+
// [#18070] The authz resolver's own reads, registered and synced so the
208+
// driver PROVISIONS them rather than refusing them.
209+
for (const o of AUTHZ_RESOLVER_OBJECTS) {
210+
ql.registerObject(o.def, o.owner);
211+
await ql.syncObjectSchema(o.def.name);
212+
}
109213

110214
const httpServer = kernel.getService<IHttpServer>('http.server');
111215
baseUrl = `http://localhost:${httpServer.getPort!()}`;

0 commit comments

Comments
 (0)