@ultimat3/cli 22.0.0 → 22.1.0
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.1.0",
|
|
4
4
|
"description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,33 +38,33 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@babel/core": "^8.0.1",
|
|
41
|
-
"@ultimat3/action": "22.
|
|
42
|
-
"@ultimat3/admin": "22.
|
|
43
|
-
"@ultimat3/ai": "22.
|
|
44
|
-
"@ultimat3/auth": "22.
|
|
45
|
-
"@ultimat3/cache": "22.
|
|
46
|
-
"@ultimat3/core": "22.
|
|
47
|
-
"@ultimat3/db": "22.
|
|
48
|
-
"@ultimat3/entity": "22.
|
|
49
|
-
"@ultimat3/flags": "22.
|
|
50
|
-
"@ultimat3/http": "22.
|
|
51
|
-
"@ultimat3/i18n": "22.
|
|
52
|
-
"@ultimat3/jobs": "22.
|
|
53
|
-
"@ultimat3/mail": "22.
|
|
54
|
-
"@ultimat3/manifest": "22.
|
|
55
|
-
"@ultimat3/mcp": "22.
|
|
56
|
-
"@ultimat3/money": "22.
|
|
57
|
-
"@ultimat3/notify": "22.
|
|
58
|
-
"@ultimat3/policy": "22.
|
|
59
|
-
"@ultimat3/pwa": "22.
|
|
60
|
-
"@ultimat3/query": "22.
|
|
61
|
-
"@ultimat3/realtime": "22.
|
|
62
|
-
"@ultimat3/render": "22.
|
|
63
|
-
"@ultimat3/schema": "22.
|
|
64
|
-
"@ultimat3/seo": "22.
|
|
65
|
-
"@ultimat3/storage": "22.
|
|
66
|
-
"@ultimat3/testing": "22.
|
|
67
|
-
"@ultimat3/time": "22.
|
|
41
|
+
"@ultimat3/action": "22.1.0",
|
|
42
|
+
"@ultimat3/admin": "22.1.0",
|
|
43
|
+
"@ultimat3/ai": "22.1.0",
|
|
44
|
+
"@ultimat3/auth": "22.1.0",
|
|
45
|
+
"@ultimat3/cache": "22.1.0",
|
|
46
|
+
"@ultimat3/core": "22.1.0",
|
|
47
|
+
"@ultimat3/db": "22.1.0",
|
|
48
|
+
"@ultimat3/entity": "22.1.0",
|
|
49
|
+
"@ultimat3/flags": "22.1.0",
|
|
50
|
+
"@ultimat3/http": "22.1.0",
|
|
51
|
+
"@ultimat3/i18n": "22.1.0",
|
|
52
|
+
"@ultimat3/jobs": "22.1.0",
|
|
53
|
+
"@ultimat3/mail": "22.1.0",
|
|
54
|
+
"@ultimat3/manifest": "22.1.0",
|
|
55
|
+
"@ultimat3/mcp": "22.1.0",
|
|
56
|
+
"@ultimat3/money": "22.1.0",
|
|
57
|
+
"@ultimat3/notify": "22.1.0",
|
|
58
|
+
"@ultimat3/policy": "22.1.0",
|
|
59
|
+
"@ultimat3/pwa": "22.1.0",
|
|
60
|
+
"@ultimat3/query": "22.1.0",
|
|
61
|
+
"@ultimat3/realtime": "22.1.0",
|
|
62
|
+
"@ultimat3/render": "22.1.0",
|
|
63
|
+
"@ultimat3/schema": "22.1.0",
|
|
64
|
+
"@ultimat3/seo": "22.1.0",
|
|
65
|
+
"@ultimat3/storage": "22.1.0",
|
|
66
|
+
"@ultimat3/testing": "22.1.0",
|
|
67
|
+
"@ultimat3/time": "22.1.0",
|
|
68
68
|
"babel-preset-solid": "^1.9.15"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/src/mcp-ui.ts
CHANGED
|
@@ -95,6 +95,8 @@ export interface UiHostInput {
|
|
|
95
95
|
readonly driver?: ((viewport: UiShotInput['viewport']) => Promise<ShotDriver>) | undefined;
|
|
96
96
|
/** Injected by a test: the route table, in place of the registry. */
|
|
97
97
|
readonly routes?: (() => readonly DeclaredRoute[]) | undefined;
|
|
98
|
+
/** Injected by a test: the settle wait, in place of `DEFAULT_SETTLE_MS` (a real sleep per call). */
|
|
99
|
+
readonly settleMs?: number | undefined;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
export interface UiCapabilities {
|
|
@@ -163,7 +165,7 @@ export function uiCapabilities(input: UiHostInput): UiCapabilities {
|
|
|
163
165
|
outDir,
|
|
164
166
|
driver,
|
|
165
167
|
boot,
|
|
166
|
-
settleMs: DEFAULT_SETTLE_MS,
|
|
168
|
+
settleMs: input.settleMs ?? DEFAULT_SETTLE_MS,
|
|
167
169
|
timeoutMs: DEFAULT_PAGE_TIMEOUT_MS,
|
|
168
170
|
fullPage: shot.fullPage,
|
|
169
171
|
colorScheme: shot.colorScheme,
|
|
@@ -197,7 +199,7 @@ export function uiCapabilities(input: UiHostInput): UiCapabilities {
|
|
|
197
199
|
root,
|
|
198
200
|
island: island.island,
|
|
199
201
|
...(island.state === undefined ? {} : { state: island.state }),
|
|
200
|
-
settleMs: DEFAULT_SETTLE_MS,
|
|
202
|
+
settleMs: input.settleMs ?? DEFAULT_SETTLE_MS,
|
|
201
203
|
timeoutMs: DEFAULT_PAGE_TIMEOUT_MS,
|
|
202
204
|
...(executablePath === undefined ? {} : { executablePath }),
|
|
203
205
|
...(cdpUrl === undefined ? {} : { cdpUrl }),
|
package/src/role-sync.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { neighbouringPort, PORT_RANGE, portPairAfter } from './flag-number';
|
|
|
20
20
|
import { portFree } from './port-probe';
|
|
21
21
|
import type { StartRolesOptions } from './role-start';
|
|
22
22
|
import { syncAuthenticator } from './sync-authenticator';
|
|
23
|
+
import { syncOriginsFrom } from './sync-url';
|
|
23
24
|
import { DEV_BINDING } from './web-binding';
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -201,6 +202,9 @@ export async function prepareSync(options: StartRolesOptions): Promise<PreparedS
|
|
|
201
202
|
buildId: options.buildId,
|
|
202
203
|
sockets,
|
|
203
204
|
...(authenticate === undefined ? {} : { authenticate }),
|
|
205
|
+
// The page's origin when it is served on another host than this node; the node's own host is
|
|
206
|
+
// admitted without it. Anything else is refused before `authenticate` (X_SOCKET_ORIGIN_REFUSED).
|
|
207
|
+
allowedOrigins: syncOriginsFrom(options.env),
|
|
204
208
|
// Tier 1 is presence, and without a registry the node answers a topic subscribe with no member
|
|
205
209
|
// list at all — the KV bucket the transport just created would hold nothing and every `sync`
|
|
206
210
|
// container would run a presence-less protocol. It reads and writes `transport.shared`, so it
|
package/src/sync-url.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Where a page's one socket dials — the framework's answer, so no app owns a `sync-url.ts`. Read
|
|
2
|
-
// once at boot from the deployment's env and handed to every document as `ultimate-sync
|
|
2
|
+
// once at boot from the deployment's env and handed to every document as `ultimate-sync` — and
|
|
3
|
+
// the page origin the node admits, which is the same question asked from the other end.
|
|
3
4
|
|
|
4
5
|
import { ConfigInvalidError } from '@ultimat3/core';
|
|
5
6
|
|
|
@@ -29,3 +30,22 @@ export function syncUrlFrom(env: Readonly<Record<string, string | undefined>>):
|
|
|
29
30
|
}
|
|
30
31
|
return declared;
|
|
31
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The page origins a sync node admits besides its own host name: `APP_URL`'s, when set. Only a
|
|
36
|
+
* deployment that serves the page on another host than the node needs it; an unparsable value is
|
|
37
|
+
* refused rather than read as "no page origin", which would refuse every socket in silence.
|
|
38
|
+
*/
|
|
39
|
+
export function syncOriginsFrom(env: Readonly<Record<string, string | undefined>>): string[] {
|
|
40
|
+
const declared = env['APP_URL']?.trim() ?? '';
|
|
41
|
+
if (declared === '') return [];
|
|
42
|
+
const parsed = URL.parse(declared);
|
|
43
|
+
if (parsed === null || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {
|
|
44
|
+
throw new ConfigInvalidError({
|
|
45
|
+
cause: 'APP_URL is set but is not an http:// or https:// URL, so no page origin can match it',
|
|
46
|
+
fix: 'export APP_URL="https://www.example.com" # the origin your pages are served on',
|
|
47
|
+
meta: { key: 'APP_URL' },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return [parsed.origin];
|
|
51
|
+
}
|
|
@@ -138,7 +138,7 @@ services:
|
|
|
138
138
|
# (X_REALTIME_TOPOLOGY). To turn live queries and channels on: declare realtime: { enabled:
|
|
139
139
|
# true, transport: 'nats', urlEnv: 'NATS_URL' } in app.config.ts, add a NATS service and set the
|
|
140
140
|
# SAME NATS_URL for web, sync and a replicator service (ROLE=replicator, exactly one), start
|
|
141
|
-
# Postgres with wal_level=logical and a
|
|
141
|
+
# Postgres with wal_level=logical and a REPLICATION role on a cluster dedicated to this app (the replicator creates its publication at boot), then set replicas: 1.
|
|
142
142
|
# A NATS_URL under transport 'memory' refuses the boot (X_CONFIG_INVALID), and so does 'nats'
|
|
143
143
|
# without one.
|
|
144
144
|
deploy: { replicas: 0 } # when on: scales on concurrent websockets — pinned to 1 by the port
|
|
@@ -29,6 +29,12 @@ export const SCAFFOLD_ENV_SCHEMA = {
|
|
|
29
29
|
role: 'sync',
|
|
30
30
|
description: 'Realtime fan-out cluster. Only the sync role is asked for it.',
|
|
31
31
|
},
|
|
32
|
+
APP_URL: {
|
|
33
|
+
type: 'url',
|
|
34
|
+
required: false,
|
|
35
|
+
role: ['web', 'sync'],
|
|
36
|
+
description: 'Public origin of the pages. Sync admits sockets from it.',
|
|
37
|
+
},
|
|
32
38
|
SYNC_URL: {
|
|
33
39
|
type: 'string',
|
|
34
40
|
required: false,
|
|
@@ -95,7 +95,7 @@ roles:
|
|
|
95
95
|
# urlEnv: 'NATS_URL' } in app.config.ts (a NATS_URL under transport 'memory' is refused, and
|
|
96
96
|
# 'nats' without it is too); run NATS and set the SAME NATS_URL for web, sync and the
|
|
97
97
|
# replicator; enable exactly one replicator (below) against a Postgres started with
|
|
98
|
-
# wal_level=logical and a
|
|
98
|
+
# wal_level=logical (the replicator creates its publication at boot) and a role with REPLICATION on a cluster dedicated to this app; then set this to true.
|
|
99
99
|
enabled: false
|
|
100
100
|
replicas: 2
|
|
101
101
|
port: 3001
|