@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.0.0",
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.0.0",
42
- "@ultimat3/admin": "22.0.0",
43
- "@ultimat3/ai": "22.0.0",
44
- "@ultimat3/auth": "22.0.0",
45
- "@ultimat3/cache": "22.0.0",
46
- "@ultimat3/core": "22.0.0",
47
- "@ultimat3/db": "22.0.0",
48
- "@ultimat3/entity": "22.0.0",
49
- "@ultimat3/flags": "22.0.0",
50
- "@ultimat3/http": "22.0.0",
51
- "@ultimat3/i18n": "22.0.0",
52
- "@ultimat3/jobs": "22.0.0",
53
- "@ultimat3/mail": "22.0.0",
54
- "@ultimat3/manifest": "22.0.0",
55
- "@ultimat3/mcp": "22.0.0",
56
- "@ultimat3/money": "22.0.0",
57
- "@ultimat3/notify": "22.0.0",
58
- "@ultimat3/policy": "22.0.0",
59
- "@ultimat3/pwa": "22.0.0",
60
- "@ultimat3/query": "22.0.0",
61
- "@ultimat3/realtime": "22.0.0",
62
- "@ultimat3/render": "22.0.0",
63
- "@ultimat3/schema": "22.0.0",
64
- "@ultimat3/seo": "22.0.0",
65
- "@ultimat3/storage": "22.0.0",
66
- "@ultimat3/testing": "22.0.0",
67
- "@ultimat3/time": "22.0.0",
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 publication for the entity tables, then set replicas: 1.
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 publication for the entity tables; then set this to true.
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