@sentropic/artifacts 0.2.3 → 0.2.4
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
CHANGED
|
@@ -76,7 +76,69 @@ Reconcile current authority independently of the restored ACL snapshot. Fresh
|
|
|
76
76
|
authorized sessions create fresh flows; revoked readers stay denied. Rollback
|
|
77
77
|
disables activation while retaining the additive schema and accepted artifacts.
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
### Importable host conformance (UP-T-2, 0.2.4)
|
|
80
|
+
|
|
81
|
+
The packed package exports the framework-independent host suite and its stable
|
|
82
|
+
vector IDs. Install the SQL asset through your test migration executor first:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import assert from 'node:assert/strict';
|
|
86
|
+
import {
|
|
87
|
+
POSTGRES_ACTIVATION_VECTOR_IDS, runPostgresActivationConformance,
|
|
88
|
+
} from '@sentropic/artifacts/adapters/postgres-activation/vectors';
|
|
89
|
+
|
|
90
|
+
assert.equal(process.env.ACTIVATION_DISPOSABLE, '1');
|
|
91
|
+
const { rows: [{ databaseName }] } = await pool.query('SELECT current_database() AS "databaseName"');
|
|
92
|
+
assert.match(databaseName, /^test[_-][a-z0-9_-]+$/i); // Check before any schema setup/reset.
|
|
93
|
+
const passed = await runPostgresActivationConformance({
|
|
94
|
+
disposable: { confirmed: true, databaseName },
|
|
95
|
+
createStore: namespace => createPostgresActivationStore({ pool, namespace }),
|
|
96
|
+
inspectTicket, // Host query returning persisted issuedAt/expiresAt and current DB now, in milliseconds.
|
|
97
|
+
createStoreWithCommitAckLoss, // Host composition with acknowledgement loss AFTER real consume COMMIT.
|
|
98
|
+
wait: async (ms, client) => { await (client ?? pool).query('SELECT pg_sleep($1)', [ms / 1000]); },
|
|
99
|
+
onPass: id => console.log('activation vector passed:', id),
|
|
100
|
+
});
|
|
101
|
+
assert.deepEqual(passed, POSTGRES_ACTIVATION_VECTOR_IDS); // 16 vectors
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Supply your host's store composition in `createStore`, using the supplied namespace
|
|
105
|
+
and the same disposable PostgreSQL database/executor for every instance. Replica
|
|
106
|
+
instances must share durable state. `wait` uses database time and the supplied
|
|
107
|
+
leased client during authority checks, including when the pool has one connection.
|
|
108
|
+
`inspectTicket(namespace, ticketId)` reads `issued_ms`, `expires_ms` and
|
|
109
|
+
`floor(extract(epoch FROM clock_timestamp()) * 1000)` through that same executor.
|
|
110
|
+
`createStoreWithCommitAckLoss(namespace)` wraps the host's leased client: execute
|
|
111
|
+
COMMIT, then throw before returning its acknowledgement. Seed with the normal
|
|
112
|
+
store; inject only into the store returned by this hook. Both hooks are optional
|
|
113
|
+
in the interface for additive compatibility; their vectors always run and fail
|
|
114
|
+
if either hook is absent. A complete host fixture is in
|
|
115
|
+
`packages/cluster-mesh/tests/packaging/activation-vectors.spec.ts`.
|
|
116
|
+
Run in an isolated process: the clock-skew vector temporarily replaces `Date.now`
|
|
117
|
+
by +/-120 seconds and restores it even on failure. TypeScript hosts install
|
|
118
|
+
`@types/pg` alongside `pg` in their consumer project.
|
|
119
|
+
The runner throws with the failing vector ID and original cause; it returns all
|
|
120
|
+
16 IDs only after every assertion passes. It uses fresh namespaces and invalidates
|
|
121
|
+
and cleans its records after each vector. Namespace rows remain in the disposable
|
|
122
|
+
database. The runner refuses without explicit disposable confirmation and an
|
|
123
|
+
actual database name starting `test_` or `test-`. Check `current_database()` on the
|
|
124
|
+
host executor before installing/resetting the schema. Never use a serving database.
|
|
125
|
+
|
|
126
|
+
The vectors cover namespace isolation, immutable records, all nine exact binding
|
|
127
|
+
comparisons, wrong IDs, denial/outage preservation, single use, concurrent replicas,
|
|
128
|
+
expiry without cleanup and after authority waits, bounded cleanup, invalidation
|
|
129
|
+
and fresh recovery, the 60-second TTL cap, flow/ticket association, DB-time-only
|
|
130
|
+
issuance/expiry under app clock skew, and `ActivationCommitUnknownError` without
|
|
131
|
+
a grant followed by replay denial. This adds
|
|
132
|
+
only a testing export in 0.2.4; adapter behavior and SQL bytes are unchanged.
|
|
133
|
+
|
|
134
|
+
`make -f packages/cluster-mesh/packaging.mk test-lazy-package-activation ENV=test-artifacts-upt2`
|
|
135
|
+
packs and installs the candidate in a clean consumer, verifies the SQL hash, and
|
|
136
|
+
runs all 16 vectors on that consumer's PostgreSQL executor (pool max 1 and 4).
|
|
137
|
+
It requires explicit disposable configuration and checks the JSON result for
|
|
138
|
+
nine passing tests and zero skipped tests; missing CI wiring fails. Follow with
|
|
139
|
+
`make down ENV=test-artifacts-upt2` (use the shared Docker lock for both commands).
|
|
140
|
+
|
|
141
|
+
Additional fault/process conformance lives in `tests/postgres-activation.test.ts` and
|
|
80
142
|
`tests/postgres-activation.integration.test.ts`, with the independent process
|
|
81
143
|
witness `tests/fixtures/activation-process.mjs`. Run through
|
|
82
144
|
`make test-artifacts` and `make test-artifacts-integration` on an isolated test
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { PoolClient } from 'pg';
|
|
2
|
+
import { type PostgresActivationStore } from './postgres-activation.js';
|
|
3
|
+
export declare const POSTGRES_ACTIVATION_VECTOR_IDS: readonly ["namespace-isolation", "immutable-records", "binding-mismatch", "wrong-ids", "authority-denial", "authority-outage", "single-use", "concurrent-consume", "expiry-without-cleanup", "expiry-after-authority", "bounded-cleanup", "invalidation-recovery", "ttl-cap", "flow-association", "db-time-skew", "commit-unknown"];
|
|
4
|
+
export type PostgresActivationVectorId = typeof POSTGRES_ACTIVATION_VECTOR_IDS[number];
|
|
5
|
+
export interface PostgresActivationConformanceHost {
|
|
6
|
+
/** Explicit opt-in; databaseName must be the executor's actual current_database(), starting test_ or test-. */
|
|
7
|
+
disposable?: {
|
|
8
|
+
confirmed: true;
|
|
9
|
+
databaseName: string;
|
|
10
|
+
};
|
|
11
|
+
/** Same disposable DB/executor for every namespace and replica; schema installed by the host. */
|
|
12
|
+
createStore(namespace: string): PostgresActivationStore;
|
|
13
|
+
/** Wait on DB time; use the supplied leased client during authority checks (supports max:1). */
|
|
14
|
+
wait(milliseconds: number, client?: PoolClient): Promise<void>;
|
|
15
|
+
/** Read persisted issuance/expiry and current DB milliseconds through the host executor. */
|
|
16
|
+
inspectTicket?(namespace: string, ticketId: string): Promise<{
|
|
17
|
+
issuedAt: number;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
now: number;
|
|
20
|
+
}>;
|
|
21
|
+
/** Same host composition, but throw AFTER the consume COMMIT succeeds, losing its acknowledgement. */
|
|
22
|
+
createStoreWithCommitAckLoss?(namespace: string): PostgresActivationStore;
|
|
23
|
+
onPass?(id: PostgresActivationVectorId): void;
|
|
24
|
+
}
|
|
25
|
+
/** Test-only, framework-independent host conformance. Throws on the first failing vector.
|
|
26
|
+
* Uses fresh isolated namespaces; invalidates and collects its own records after each vector.
|
|
27
|
+
* Process crashes and restore snapshots remain package integration tests.
|
|
28
|
+
* Optional hooks preserve interface compatibility; their vectors are mandatory and fail if absent.
|
|
29
|
+
* Run in an isolated process: db-time-skew temporarily replaces Date.now and always restores it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runPostgresActivationConformance(host: PostgresActivationConformanceHost): Promise<PostgresActivationVectorId[]>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { ActivationCommitUnknownError } from './postgres-activation.js';
|
|
4
|
+
export const POSTGRES_ACTIVATION_VECTOR_IDS = Object.freeze([
|
|
5
|
+
'namespace-isolation', 'immutable-records', 'binding-mismatch', 'wrong-ids',
|
|
6
|
+
'authority-denial', 'authority-outage', 'single-use', 'concurrent-consume',
|
|
7
|
+
'expiry-without-cleanup', 'expiry-after-authority', 'bounded-cleanup',
|
|
8
|
+
'invalidation-recovery', 'ttl-cap', 'flow-association',
|
|
9
|
+
'db-time-skew', 'commit-unknown',
|
|
10
|
+
]);
|
|
11
|
+
/** Test-only, framework-independent host conformance. Throws on the first failing vector.
|
|
12
|
+
* Uses fresh isolated namespaces; invalidates and collects its own records after each vector.
|
|
13
|
+
* Process crashes and restore snapshots remain package integration tests.
|
|
14
|
+
* Optional hooks preserve interface compatibility; their vectors are mandatory and fail if absent.
|
|
15
|
+
* Run in an isolated process: db-time-skew temporarily replaces Date.now and always restores it.
|
|
16
|
+
*/
|
|
17
|
+
export async function runPostgresActivationConformance(host) {
|
|
18
|
+
assert.ok(host.disposable?.confirmed === true, 'Explicit disposable database confirmation is required');
|
|
19
|
+
assert.match(host.disposable.databaseName, /^test[_-][a-z0-9_-]+$/i, 'A test database name is required');
|
|
20
|
+
const passed = [];
|
|
21
|
+
for (const id of POSTGRES_ACTIVATION_VECTOR_IDS) {
|
|
22
|
+
const namespace = `upt2:${randomUUID()}`;
|
|
23
|
+
const store = host.createStore(namespace);
|
|
24
|
+
const foreign = host.createStore(`${namespace}:other`);
|
|
25
|
+
const expected = { subjectId: 'alice', artifactId: 'a', releaseId: 'r', channel: 'stable',
|
|
26
|
+
nonceDigest: 'ab'.repeat(32), sessionId: 'session', profileDigest: 'cd'.repeat(32),
|
|
27
|
+
policyRevision: 'p1', targetOrigin: 'https://activation.example.test' };
|
|
28
|
+
async function seed(ttl = 50_000, target = store) {
|
|
29
|
+
assert.ok(target.tickets.issue, 'DB-time ticket issuance is required');
|
|
30
|
+
const ticket = await target.tickets.issue({ ...expected, ticketId: randomUUID() }, ttl);
|
|
31
|
+
const flow = { ...expected, ticketId: ticket.ticketId, flowId: randomUUID(), expiresAt: ticket.expiresAt };
|
|
32
|
+
await target.flows.put(flow);
|
|
33
|
+
return { ticket, flow, input: { ticketId: ticket.ticketId, flowId: flow.flowId, expected } };
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const { ticket, flow, input } = await seed();
|
|
37
|
+
const redeem = () => store.consume(input, async () => true);
|
|
38
|
+
const preserved = async () => {
|
|
39
|
+
assert.deepEqual(await store.tickets.peek(ticket.ticketId), ticket);
|
|
40
|
+
assert.deepEqual(await store.flows.peek(flow.flowId), flow);
|
|
41
|
+
};
|
|
42
|
+
if (id === 'namespace-isolation') {
|
|
43
|
+
assert.equal(await foreign.tickets.peek(ticket.ticketId), undefined);
|
|
44
|
+
assert.equal(await foreign.flows.peek(flow.flowId), undefined);
|
|
45
|
+
assert.equal(await foreign.consume(input, async () => true), undefined);
|
|
46
|
+
await foreign.tickets.remove(ticket.ticketId);
|
|
47
|
+
await preserved();
|
|
48
|
+
}
|
|
49
|
+
else if (id === 'immutable-records') {
|
|
50
|
+
await assert.rejects(store.tickets.put({ ...ticket, subjectId: 'mallory' }));
|
|
51
|
+
await assert.rejects(store.flows.put({ ...flow, sessionId: 'other' }));
|
|
52
|
+
await preserved();
|
|
53
|
+
}
|
|
54
|
+
else if (id === 'binding-mismatch') {
|
|
55
|
+
for (const key of Object.keys(expected)) {
|
|
56
|
+
const mismatch = { ...expected, [key]: key === 'channel' ? 'pinned' : 'different' };
|
|
57
|
+
assert.equal(await store.consume({ ...input, expected: mismatch }, async () => true), undefined, key);
|
|
58
|
+
}
|
|
59
|
+
await preserved();
|
|
60
|
+
assert.deepEqual(await redeem(), ticket);
|
|
61
|
+
}
|
|
62
|
+
else if (id === 'wrong-ids') {
|
|
63
|
+
for (const key of ['ticketId', 'flowId']) {
|
|
64
|
+
assert.equal(await store.consume({ ...input, [key]: randomUUID() }, async () => true), undefined);
|
|
65
|
+
}
|
|
66
|
+
await preserved();
|
|
67
|
+
}
|
|
68
|
+
else if (id === 'authority-denial') {
|
|
69
|
+
assert.equal(await store.consume(input, async () => false), undefined);
|
|
70
|
+
await preserved();
|
|
71
|
+
}
|
|
72
|
+
else if (id === 'authority-outage') {
|
|
73
|
+
const outage = new Error('conformance authority unavailable');
|
|
74
|
+
await assert.rejects(store.consume(input, async () => { throw outage; }), error => error === outage);
|
|
75
|
+
await preserved();
|
|
76
|
+
}
|
|
77
|
+
else if (id === 'single-use') {
|
|
78
|
+
assert.deepEqual(await redeem(), ticket);
|
|
79
|
+
assert.equal(await redeem(), undefined);
|
|
80
|
+
assert.equal(await store.flows.peek(flow.flowId), undefined);
|
|
81
|
+
}
|
|
82
|
+
else if (id === 'concurrent-consume') {
|
|
83
|
+
const replica = host.createStore(namespace);
|
|
84
|
+
const results = await Promise.all([redeem(), replica.consume(input, async () => true)]);
|
|
85
|
+
assert.deepEqual(results.filter(Boolean), [ticket]);
|
|
86
|
+
}
|
|
87
|
+
else if (id === 'expiry-without-cleanup' || id === 'expiry-after-authority') {
|
|
88
|
+
const expired = await seed(1000);
|
|
89
|
+
if (id === 'expiry-without-cleanup')
|
|
90
|
+
await host.wait(1200);
|
|
91
|
+
assert.equal(await store.consume(expired.input, async ({ client }) => {
|
|
92
|
+
await host.wait(1200, client);
|
|
93
|
+
return true;
|
|
94
|
+
}), undefined);
|
|
95
|
+
assert.equal(await store.tickets.peek(expired.ticket.ticketId), undefined);
|
|
96
|
+
assert.equal(await store.flows.peek(expired.flow.flowId), undefined);
|
|
97
|
+
}
|
|
98
|
+
else if (id === 'bounded-cleanup') {
|
|
99
|
+
for (let i = 0; i < 3; i++)
|
|
100
|
+
await seed(1000);
|
|
101
|
+
const other = await seed(50_000, foreign);
|
|
102
|
+
await host.wait(1200);
|
|
103
|
+
let total = 0;
|
|
104
|
+
for (let i = 0; i < 4; i++) {
|
|
105
|
+
const removed = await store.cleanup(2);
|
|
106
|
+
const count = removed.flows + removed.tickets;
|
|
107
|
+
assert.ok(count >= 0 && count <= 2);
|
|
108
|
+
total += count;
|
|
109
|
+
}
|
|
110
|
+
assert.equal(total, 6);
|
|
111
|
+
await preserved();
|
|
112
|
+
assert.deepEqual(await foreign.tickets.peek(other.ticket.ticketId), other.ticket);
|
|
113
|
+
}
|
|
114
|
+
else if (id === 'invalidation-recovery') {
|
|
115
|
+
await store.invalidate();
|
|
116
|
+
const replica = host.createStore(namespace);
|
|
117
|
+
assert.equal(await replica.consume(input, async () => true), undefined);
|
|
118
|
+
assert.equal(await replica.tickets.peek(ticket.ticketId), undefined);
|
|
119
|
+
assert.equal(await replica.flows.peek(flow.flowId), undefined);
|
|
120
|
+
const fresh = await seed();
|
|
121
|
+
assert.deepEqual(await replica.consume(fresh.input, async () => true), fresh.ticket);
|
|
122
|
+
}
|
|
123
|
+
else if (id === 'ttl-cap') {
|
|
124
|
+
for (const ttl of [0, 60_001]) {
|
|
125
|
+
await assert.rejects(store.tickets.issue({ ...expected, ticketId: randomUUID() }, ttl));
|
|
126
|
+
}
|
|
127
|
+
await seed(60_000);
|
|
128
|
+
}
|
|
129
|
+
else if (id === 'flow-association') {
|
|
130
|
+
await assert.rejects(store.flows.put({ ...flow, flowId: randomUUID(), subjectId: 'mallory' }));
|
|
131
|
+
await assert.rejects(store.flows.put({ ...flow, flowId: randomUUID(), expiresAt: ticket.expiresAt + 1 }));
|
|
132
|
+
}
|
|
133
|
+
else if (id === 'db-time-skew') {
|
|
134
|
+
assert.ok(host.inspectTicket, 'inspectTicket hook is required for db-time-skew');
|
|
135
|
+
const originalNow = Date.now;
|
|
136
|
+
try {
|
|
137
|
+
for (const skew of [-120_000, 120_000]) {
|
|
138
|
+
Date.now = () => originalNow() + skew;
|
|
139
|
+
const issued = await seed(60_000);
|
|
140
|
+
const db = await host.inspectTicket(namespace, issued.ticket.ticketId);
|
|
141
|
+
assert.equal(db.expiresAt - db.issuedAt, 60_000);
|
|
142
|
+
assert.equal(issued.ticket.expiresAt, db.expiresAt);
|
|
143
|
+
assert.ok(db.expiresAt - db.now > 59_000 && db.expiresAt - db.now <= 60_000);
|
|
144
|
+
assert.deepEqual(await store.consume(issued.input, async () => true), issued.ticket);
|
|
145
|
+
const expired = await seed(50);
|
|
146
|
+
await host.wait(80);
|
|
147
|
+
assert.equal(await store.consume(expired.input, async () => true), undefined);
|
|
148
|
+
assert.equal(await store.tickets.peek(expired.ticket.ticketId), undefined);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
Date.now = originalNow;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else if (id === 'commit-unknown') {
|
|
156
|
+
assert.ok(host.createStoreWithCommitAckLoss, 'createStoreWithCommitAckLoss hook is required for commit-unknown');
|
|
157
|
+
const uncertain = host.createStoreWithCommitAckLoss(namespace);
|
|
158
|
+
const grants = [];
|
|
159
|
+
await assert.rejects(uncertain.consume(input, async () => true).then(result => grants.push(result)), error => error instanceof ActivationCommitUnknownError);
|
|
160
|
+
assert.deepEqual(grants, []);
|
|
161
|
+
assert.equal(await host.createStore(namespace).consume(input, async () => true), undefined);
|
|
162
|
+
assert.equal(await store.flows.peek(flow.flowId), undefined);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (cause) {
|
|
166
|
+
throw new Error(`UP-T conformance failed: ${id}`, { cause });
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
for (const target of [store, foreign]) {
|
|
170
|
+
await target.invalidate();
|
|
171
|
+
await target.cleanup(1000);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
passed.push(id);
|
|
175
|
+
host.onPass?.(id);
|
|
176
|
+
}
|
|
177
|
+
return passed;
|
|
178
|
+
}
|
package/dist/mcp-core.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { createRadarKeyDerivation } from './server/command-identity.js';
|
|
|
10
10
|
export type { ArtifactKeyDerivationBinding } from './server/command-identity.js';
|
|
11
11
|
export declare const ARTIFACTS_PROVIDER_ID = "sentropic.artifacts";
|
|
12
12
|
export declare const ARTIFACTS_APP_ID = "sentropic-artifacts";
|
|
13
|
-
export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.
|
|
13
|
+
export declare const ARTIFACTS_PROVIDER_VERSION = "0.2.4";
|
|
14
14
|
export declare const ARTIFACTS_TOOL_NAMES: readonly ["artifacts_get_template", "artifacts_get_api_guide", "artifacts_ensure", "artifacts_publish", "artifacts_create_upload", "artifacts_commit_upload", "artifacts_list", "artifacts_get", "artifacts_get_source", "artifacts_fork", "artifacts_set_access", "artifacts_promote", "artifacts_renew", "artifacts_delete", "artifacts_restore"];
|
|
15
15
|
export type ArtifactsToolName = (typeof ARTIFACTS_TOOL_NAMES)[number];
|
|
16
16
|
export declare const ARTIFACTS_TOOL_SPECS: {
|
package/dist/mcp-core.js
CHANGED
|
@@ -6,7 +6,7 @@ export * from './profile/index.js';
|
|
|
6
6
|
export { createRadarKeyDerivation } from './server/command-identity.js';
|
|
7
7
|
export const ARTIFACTS_PROVIDER_ID = 'sentropic.artifacts';
|
|
8
8
|
export const ARTIFACTS_APP_ID = 'sentropic-artifacts';
|
|
9
|
-
export const ARTIFACTS_PROVIDER_VERSION = '0.2.
|
|
9
|
+
export const ARTIFACTS_PROVIDER_VERSION = '0.2.4';
|
|
10
10
|
export const ARTIFACTS_TOOL_NAMES = ARTIFACT_CAPABILITY_NAMES;
|
|
11
11
|
export const ARTIFACTS_TOOL_SPECS = ARTIFACTS_TOOL_NAMES.map((name) => ({ name,
|
|
12
12
|
description: `Artifacts ${name.slice('artifacts_'.length).replaceAll('_', ' ')}.`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentropic/artifacts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Generic configurable artifacts MCP and artifact service: StorageScope/Artifact/Binding/Release model, canonical manifests, object authorization, idempotent intents (spec-v8 §9).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,6 +45,10 @@
|
|
|
45
45
|
"types": "./dist/adapters/postgres-activation.d.ts",
|
|
46
46
|
"import": "./dist/adapters/postgres-activation.js"
|
|
47
47
|
},
|
|
48
|
+
"./adapters/postgres-activation/vectors": {
|
|
49
|
+
"types": "./dist/adapters/postgres-activation-vectors.d.ts",
|
|
50
|
+
"import": "./dist/adapters/postgres-activation-vectors.js"
|
|
51
|
+
},
|
|
48
52
|
"./sql/*": "./sql/*",
|
|
49
53
|
"./migrations/*": "./migrations/*",
|
|
50
54
|
"./server/ports": {
|