@feasibleone/blong-gogo 1.25.0 → 1.26.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/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.26.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.25.1...blong-gogo-v1.26.0) (2026-07-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * authorization ([cf1a7ff](https://github.com/feasibleone/blong/commit/cf1a7ff605de0a6078fc859a31532443386808b2))
9
+ * blong-mock, blong-server ([756caca](https://github.com/feasibleone/blong/commit/756cacab712db6792bf1b93830fc1d833b396f8d))
10
+ * blong-party ([602a3b4](https://github.com/feasibleone/blong/commit/602a3b422e1f76c27a96b1cd5d5a8cf0edbc268f))
11
+ * check access credentials ([e6af405](https://github.com/feasibleone/blong/commit/e6af405c43921cb9c553db9c4986b7fe56154724))
12
+ * core.resource merging ([371d3e2](https://github.com/feasibleone/blong/commit/371d3e2b3f519dfd59ca4fd480bc33317992afcd))
13
+ * implement subjectObjectSchema ([8ca3282](https://github.com/feasibleone/blong/commit/8ca32829277b9816eb5e43ce708d2f1d0cc3ef28))
14
+ * improve playwright ([c88dca8](https://github.com/feasibleone/blong/commit/c88dca832548a878ce81aab7320395c3c6a81551))
15
+ * manifest concept ([ed3c038](https://github.com/feasibleone/blong/commit/ed3c038352644d2d8f828b3cb69cbea718e7084b))
16
+ * object schema ([688d5d2](https://github.com/feasibleone/blong/commit/688d5d2a4a895c8feba7b898da03c46b3a3f3292))
17
+ * playwright coverage ([616cf61](https://github.com/feasibleone/blong/commit/616cf61f8616dc7fa6fac8f75fcd5a8577e70448))
18
+ * schema helpers and constraints ([8409257](https://github.com/feasibleone/blong/commit/84092571f6b327c1166ef142f70cb7d91b013eaf))
19
+ * schema sync ([ae63684](https://github.com/feasibleone/blong/commit/ae63684ea85d2b0f7adc8e8a380e51e0564d54c4))
20
+ * seed ([5c76603](https://github.com/feasibleone/blong/commit/5c7660392bc1197638810e864bfd47ce1218b2e5))
21
+ * support expressions in the configuration ([843bb58](https://github.com/feasibleone/blong/commit/843bb58c97456d85eac573874c90755bdb68bc95))
22
+ * vitest coverage ([4c6d9cb](https://github.com/feasibleone/blong/commit/4c6d9cb941957b0291cbe091e06884589b1e8f06))
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * authorization flow test ([f816dc2](https://github.com/feasibleone/blong/commit/f816dc2fb0d1556dff04d42901cfe5c36cca0717))
28
+ * build ([de22713](https://github.com/feasibleone/blong/commit/de2271393350945b8d793ca0ec89670465bba085))
29
+ * core.resource merging ([f191055](https://github.com/feasibleone/blong/commit/f1910558ce0f369f556911c4c0f6397b8003fd33))
30
+ * full stack alignment for CRUD ([8795e08](https://github.com/feasibleone/blong/commit/8795e087182402ac28c3ea03c6bee27319069625))
31
+ * handle existing PK ([fb0041b](https://github.com/feasibleone/blong/commit/fb0041b8f6ac7ba2f0dc0a752ab19dd49c09d79f))
32
+ * hanging playwright install ([13bf7a4](https://github.com/feasibleone/blong/commit/13bf7a4031f1f94160e0cb9d8b4f83a1732e70af))
33
+ * keep running the server in CI for the playwright intent ([5d01f3b](https://github.com/feasibleone/blong/commit/5d01f3b044f4506c2f6fab3c2c048b116b59accb))
34
+ * login ([4def73d](https://github.com/feasibleone/blong/commit/4def73d33473d2b9183e91fa1ab030b2ccb85ad5))
35
+ * Playwright, Storybook and realm DRY ([4302512](https://github.com/feasibleone/blong/commit/43025123e613ef43e0ac5b83ab00fbc0cabfccf2))
36
+ * Playwright, Storybook and realm DRY ([7bbfad8](https://github.com/feasibleone/blong/commit/7bbfad8160da0ead5600ad56b543e935bfd01954))
37
+ * portal loading in the browser ([5c2bc76](https://github.com/feasibleone/blong/commit/5c2bc762dea53d7d2caa0022ea3d7e3f106fdddb))
38
+
39
+ ## [1.25.1](https://github.com/feasibleone/blong/compare/blong-gogo-v1.25.0...blong-gogo-v1.25.1) (2026-05-25)
40
+
41
+
42
+ ### Bug Fixes
43
+
44
+ * dependencies ([cc6ba83](https://github.com/feasibleone/blong/commit/cc6ba830a721dfb063f5331d44e26a97e28c7f9a))
45
+ * improve testing ([2b1d862](https://github.com/feasibleone/blong/commit/2b1d862fda71ed7910bc99f4bbbe11ef17be098c))
46
+
3
47
  ## [1.25.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.24.0...blong-gogo-v1.25.0) (2026-05-19)
4
48
 
5
49
 
@@ -9,7 +9,7 @@ const argv: {_: string[]} = minimist(process.argv.slice(2));
9
9
 
10
10
  // The first positional arg is an optional file/folder target; the rest are intents.
11
11
  const [maybeTarget, ...rest] = argv._;
12
- const target = maybeTarget && existsSync(resolve(maybeTarget)) ? maybeTarget : undefined;
12
+ const target = maybeTarget && existsSync(resolve(maybeTarget)) ? resolve(maybeTarget) : undefined;
13
13
  const intents = target ? rest : argv._;
14
14
 
15
15
  await autoRun({cwd: process.cwd(), target, intents});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feasibleone/blong-gogo",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/feasibleone/blong.git"
6
6
  },
@@ -24,13 +24,17 @@
24
24
  "@fastify/bearer-auth": "^10.1.2",
25
25
  "@fastify/cookie": "^11.0.2",
26
26
  "@fastify/cors": "^11.2.0",
27
+ "@fastify/deepmerge": "^3.2.1",
27
28
  "@fastify/swagger": "^9.7.0",
28
29
  "@fastify/swagger-ui": "^5.2.5",
30
+ "@fastify/type-provider-typebox": "^6.1.0",
29
31
  "@feasibleone/blong": "^1.0.0",
30
32
  "@feasibleone/blong-chain": "^1.0.0",
31
33
  "@feasibleone/blong-config": "^1.0.0",
32
34
  "@feasibleone/blong-kopi": "^1.0.0",
33
- "@keycloak/keycloak-admin-client": "^26.5.4",
35
+ "@feasibleone/blong-lib": "^1.0.0",
36
+ "@feasibleone/blong-mock": "^1.0.0",
37
+ "@keycloak/keycloak-admin-client": "^26.6.2",
34
38
  "@kubernetes/client-node": "^1.4.0",
35
39
  "@octokit/rest": "^22.0.1",
36
40
  "@sinclair/typebox-codegen": "^0.11.1",
@@ -43,7 +47,7 @@
43
47
  "fastify-plugin": "^5.1.0",
44
48
  "got": "^14.6.6",
45
49
  "jose": "^6.2.1",
46
- "knex": "^3.1.0",
50
+ "knex": "^3.2.10",
47
51
  "ky": "^1.14.2",
48
52
  "lru-cache": "^11.2.4",
49
53
  "minimist": "^1.2.8",
@@ -64,10 +68,12 @@
64
68
  "undici": "^8.0.1",
65
69
  "ut-bitsyntax": "^6.2.7",
66
70
  "ut-dns-discovery": "^6.2.3",
71
+ "ut-function.cbc": "^1.1.6",
67
72
  "ut-function.interpolate": "1.1.3",
68
73
  "ut-function.merge": "^1.5.6",
69
74
  "uuid": "^13.0.0",
70
- "yaml": "^2.8.3"
75
+ "yaml": "^2.8.3",
76
+ "@feasibleone/blong-template": "1.0.0"
71
77
  },
72
78
  "devDependencies": {
73
79
  "@rushstack/heft": "^1.2.6",
@@ -75,17 +81,17 @@
75
81
  "@rushstack/heft-typescript-plugin": "^1.3.1",
76
82
  "@types/cacache": "^20.0.1",
77
83
  "@types/node": "^24",
78
- "playwright": "^1.58.2",
84
+ "playwright": "^1.60.0",
79
85
  "tap": "^21.6.3",
80
- "typescript": "^5.9.3",
81
- "@feasibleone/blong-dev": "1.0.0"
86
+ "typescript": "^6.0.3",
87
+ "@feasibleone/blong-dev": "1.1.0"
82
88
  },
83
89
  "scripts": {
84
90
  "browser-check": "node scripts/browser-compat-check.mjs",
85
91
  "build": "true",
86
- "ci-coverage": "../common/run-coverage.sh report",
92
+ "ci-coverage": "./run-coverage.sh",
87
93
  "ci-lint": "blong-dev lint",
88
94
  "ci-publish": "node ../../common/scripts/install-run-rush-pnpm.js publish --access public --provenance",
89
- "ci-unit": "../common/run-coverage.sh"
95
+ "ci-test": "blong-dev test"
90
96
  }
91
97
  }
@@ -11,7 +11,6 @@ import type {
11
11
  } from '@feasibleone/blong/types';
12
12
  import type net from 'node:net';
13
13
  import PQueue from 'p-queue';
14
- import merge from 'ut-function.merge';
15
14
 
16
15
  import ConfigRuntime from './ConfigRuntime.ts';
17
16
  import {isExpectedError} from './lib.ts';
@@ -87,6 +86,7 @@ export class AdapterBase<T, C extends IContext> implements AdapterHandlerContext
87
86
  configBase: string;
88
87
  log: unknown = null;
89
88
  importedMap?: Map<string, Record<string, (...args: unknown[]) => unknown>>;
89
+ platform: IApi['platform'];
90
90
 
91
91
  // These are prefixed with _ rather than using # private class fields.
92
92
  // The adapter uses Object.setPrototypeOf(current, base) to set the base as
@@ -103,7 +103,7 @@ export class AdapterBase<T, C extends IContext> implements AdapterHandlerContext
103
103
  // api.attachHandlers = ... only *after* the port factory returns. Storing
104
104
  // the api object and reading api.attachHandlers lazily (at call time)
105
105
  // ensures we always see the real function rather than the initial undefined.
106
- _api: Pick<IApi, 'attachHandlers'>;
106
+ _api: Pick<IApi, 'attachHandlers' | 'render' | 'attach'>;
107
107
  _createLog: IApi['createLog'];
108
108
  _attachCheckpoint: IApi['attachCheckpoint'];
109
109
  _activationNames: string[];
@@ -123,6 +123,9 @@ export class AdapterBase<T, C extends IContext> implements AdapterHandlerContext
123
123
  | 'attachHandlers'
124
124
  | 'createLog'
125
125
  | 'attachCheckpoint'
126
+ | 'render'
127
+ | 'platform'
128
+ | 'attach'
126
129
  >,
127
130
  configBase: string,
128
131
  activationNames: string[] = [],
@@ -133,6 +136,7 @@ export class AdapterBase<T, C extends IContext> implements AdapterHandlerContext
133
136
  this._dispatch = api.dispatch;
134
137
  this._methodId = api.methodId;
135
138
  this._getPath = api.getPath;
139
+ this.platform = api.platform;
136
140
  this._api = api;
137
141
  this._createLog = api.createLog;
138
142
  this._attachCheckpoint = api.attachCheckpoint;
@@ -150,7 +154,7 @@ export class AdapterBase<T, C extends IContext> implements AdapterHandlerContext
150
154
  }
151
155
 
152
156
  async init(...configs: object[]): Promise<void> {
153
- this.config = merge(this.activeConfig(), ...configs) as Config<T, C>;
157
+ this.config = this._api.render([this.activeConfig(), ...configs]) as Config<T, C>;
154
158
  this.log = this._createLog?.(this.config.logLevel || 'info', {
155
159
  ...this.config.log,
156
160
  name: this.config.id,
@@ -275,7 +279,7 @@ export class AdapterBase<T, C extends IContext> implements AdapterHandlerContext
275
279
  async drain(): Promise<void> {}
276
280
 
277
281
  findHandler(methodName: string): unknown {
278
- methodName = this._methodId(methodName);
282
+ methodName = this.methodPath(this._methodId(methodName));
279
283
  return this.imported[methodName];
280
284
  }
281
285
 
@@ -323,12 +327,12 @@ export class AdapterBase<T, C extends IContext> implements AdapterHandlerContext
323
327
  return this.event('start', {configBase: this.configBase, config});
324
328
  }
325
329
 
326
- async link(
330
+ async attach(
327
331
  patterns: (string | RegExp)[] | string | RegExp,
328
- target: AdapterHandlerContext = {} as unknown as AdapterHandlerContext,
332
+ target: object = {},
329
333
  ): Promise<object> {
330
- await this._api.attachHandlers(target, patterns);
331
- return target.imported;
334
+ await this._api.attach(target, patterns);
335
+ return target;
332
336
  }
333
337
 
334
338
  async handle(...params: unknown[]): Promise<unknown> {
@@ -371,17 +375,27 @@ export default async function adapter<T, C extends IContext>(
371
375
  configBase: string,
372
376
  activationNames: string[] = [],
373
377
  ): Promise<Adapter<T, C>> {
374
- const {adapter: adapterFactory, utError, handlers, remote, rpc, local, registry, type} = api;
378
+ const {
379
+ adapter: adapterFactory,
380
+ utError,
381
+ handlers,
382
+ remote,
383
+ rpc,
384
+ local,
385
+ registry,
386
+ type,
387
+ schema,
388
+ } = api;
375
389
  _errors ||= utError.register(errorMap);
376
390
 
377
391
  const base = new AdapterBase<T, C>(api, configBase, activationNames);
378
392
 
379
- const result = handlers!({utError, remote, type});
393
+ const result = handlers!({utError, remote, type, schema: registry.objectSchema, manifest: api.manifest});
380
394
  let current = result;
381
395
  while (current.extends) {
382
396
  const parent = await (typeof current.extends === 'string'
383
- ? adapterFactory(current.extends)!({utError, remote, rpc, local, registry})
384
- : current.extends({utError, remote, rpc, local, registry}));
397
+ ? adapterFactory(current.extends)!({utError, remote, rpc, local, registry, schema, manifest: api.manifest})
398
+ : current.extends({utError, remote, rpc, local, registry, schema, manifest: api.manifest}));
385
399
  Object.setPrototypeOf(current, parent);
386
400
  current = parent;
387
401
  }
@@ -133,7 +133,12 @@ export function createConfigProxy<T extends object>(
133
133
  get(_target, prop) {
134
134
  const container = getNode(path);
135
135
  const val = container == null ? undefined : Reflect.get(container as object, prop);
136
- if (val === undefined || val === null || typeof val !== 'object') {
136
+ if (
137
+ val === undefined ||
138
+ val === null ||
139
+ typeof val !== 'object' ||
140
+ val.constructor !== Object
141
+ ) {
137
142
  if (
138
143
  _factoryPhase.active &&
139
144
  val !== undefined &&
@@ -252,7 +257,9 @@ export function deepDiff(prev: unknown, next: unknown, path = ''): ConfigDiff {
252
257
  // ---------------------------------------------------------------------------
253
258
 
254
259
  export default class ConfigRuntime implements IConfigRuntime {
255
- readonly #loadParams: object;
260
+ readonly #baseConfig: object;
261
+ readonly #configs: object[];
262
+ readonly #suite: string;
256
263
  readonly #subscribers: Set<ConfigSubscriber> = new Set();
257
264
 
258
265
  #rawSnapshot: object = {};
@@ -262,8 +269,10 @@ export default class ConfigRuntime implements IConfigRuntime {
262
269
  readonly enterConfig: (mode?: FactoryPhaseMode) => void;
263
270
  readonly exitConfig: () => Error[];
264
271
 
265
- public constructor(loadParams: object = {}) {
266
- this.#loadParams = loadParams;
272
+ public constructor(baseConfig: object, configs: (object | boolean)[], suite: string) {
273
+ this.#baseConfig = baseConfig;
274
+ this.#configs = configs.filter(Boolean) as object[];
275
+ this.#suite = suite;
267
276
  const {proxy, update, enterConfig, exitConfig} = createConfigProxy(this.#rawSnapshot);
268
277
  this.#proxy = proxy;
269
278
  this.#updateProxy = update;
@@ -283,13 +292,18 @@ export default class ConfigRuntime implements IConfigRuntime {
283
292
  return this.#rawSnapshot;
284
293
  }
285
294
 
295
+ private mergeConfigs(blongConfig: object): object {
296
+ const loaded = loadBlong(blongConfig);
297
+ return merge({}, this.#baseConfig, ...this.#configs, loaded);
298
+ }
299
+
286
300
  /**
287
301
  * Load configuration from all sources using blong-config.
288
302
  * Merges the result into the proxy backing store so existing references
289
303
  * automatically reflect the new values.
290
304
  */
291
305
  public async load(params: object = {}): Promise<object> {
292
- const loaded = loadBlong({...this.#loadParams, ...params});
306
+ const loaded = this.mergeConfigs({config: {suite: this.#suite}, ...params});
293
307
  this.#rawSnapshot = loaded;
294
308
  this.#updateProxy(loaded);
295
309
  return this.#proxy;
@@ -304,7 +318,7 @@ export default class ConfigRuntime implements IConfigRuntime {
304
318
  */
305
319
  public async reload(): Promise<ConfigDiff> {
306
320
  const prev = this.#rawSnapshot;
307
- const next = loadBlong(this.#loadParams);
321
+ const next = this.mergeConfigs({config: {suite: this.#suite}});
308
322
  const diff = this.diff(prev, next);
309
323
 
310
324
  // Only do work when something actually changed
@@ -390,17 +404,4 @@ export default class ConfigRuntime implements IConfigRuntime {
390
404
  .flat(),
391
405
  ]);
392
406
  }
393
-
394
- /**
395
- * Merge module configs from multiple sources in the standard order.
396
- *
397
- * Previously done in `loadRealm()`:
398
- * `merge(mergedConfig, ...loadedConfigs.filter(Boolean))`
399
- *
400
- * @param base - the base config object (mutated in place)
401
- * @param sources - config objects to merge, in priority order
402
- */
403
- static mergeModuleConfig(base: object, ...sources: unknown[]): object {
404
- return merge(base, ...sources.filter(Boolean));
405
- }
406
407
  }
package/src/Gateway.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  IGateway,
8
8
  ILocal,
9
9
  ILog,
10
+ IManifest,
10
11
  IMeta,
11
12
  IPlatformApi,
12
13
  } from '@feasibleone/blong/types';
@@ -15,12 +16,14 @@ import fastify, {type FastifyReply, type FastifyRequest, type RouteOptions} from
15
16
  import os from 'os';
16
17
  import type {LevelWithSilent} from 'pino';
17
18
  import {Type, type TSchema} from 'typebox';
19
+ import Value from 'typebox/value';
18
20
  import {v4} from 'uuid';
19
21
 
22
+ import {type TypeBoxTypeProvider} from '@fastify/type-provider-typebox';
20
23
  import type {IResolution} from './Resolution.ts';
21
24
  import type {IRpcClient} from './RpcClient.ts';
22
25
  import jwt from './jwt.ts';
23
- import {isExpectedError, methodParts, snakeToCamel} from './lib.ts';
26
+ import {isExpectedError, methodId, methodParts, snakeToCamel} from './lib.ts';
24
27
  import type {IConfig as IConfigMLE} from './mle.ts';
25
28
  import swagger from './swagger.ts';
26
29
 
@@ -68,6 +71,13 @@ interface IConfig extends IConfigMLE {
68
71
  * production to prevent callers from suppressing audit-level error logs.
69
72
  */
70
73
  expectedErrors?: boolean;
74
+ /**
75
+ * Optional handler name (semantic triple) that returns the list of allowed
76
+ * actions for the authenticated user. When set, the gateway calls this
77
+ * handler during authentication and stores the result in credentials,
78
+ * then enforces that every request's methodId is in the allowed list.
79
+ */
80
+ authorize?: string;
71
81
  errorFields: unknown[];
72
82
  jwt: {
73
83
  cache: object;
@@ -146,7 +156,7 @@ export default class Gateway extends Internal implements IGateway {
146
156
  #config: IConfig = {
147
157
  host: '0.0.0.0',
148
158
  port: 8080,
149
- logLevel: 'trace',
159
+ logLevel: 'warn',
150
160
  cors: undefined,
151
161
  sign: undefined as IConfig['sign'] | undefined,
152
162
  encrypt: undefined as IConfig['encrypt'] | undefined,
@@ -156,6 +166,7 @@ export default class Gateway extends Internal implements IGateway {
156
166
  },
157
167
  debug: false,
158
168
  expectedErrors: false,
169
+ authorize: undefined,
159
170
  errorFields: [],
160
171
  jwt: {
161
172
  cache: {},
@@ -170,6 +181,7 @@ export default class Gateway extends Internal implements IGateway {
170
181
  #errorFields: [string, unknown][] = [];
171
182
  #plugins: {plugin: unknown; options: unknown}[] = [];
172
183
  #platform: IPlatformApi;
184
+ #manifest: IManifest | undefined;
173
185
 
174
186
  public constructor(
175
187
  config: IConfig,
@@ -180,6 +192,7 @@ export default class Gateway extends Internal implements IGateway {
180
192
  resolution,
181
193
  local,
182
194
  platform,
195
+ manifest,
183
196
  }: {
184
197
  log?: ILog;
185
198
  error?: IErrorFactory;
@@ -187,6 +200,7 @@ export default class Gateway extends Internal implements IGateway {
187
200
  local?: ILocal;
188
201
  resolution?: IResolution;
189
202
  platform?: IPlatformApi;
203
+ manifest?: IManifest;
190
204
  },
191
205
  ) {
192
206
  super({log});
@@ -194,6 +208,16 @@ export default class Gateway extends Internal implements IGateway {
194
208
  this.#platform = platform!;
195
209
 
196
210
  this.merge(this.#config, config);
211
+ this.#config.sign ||= (
212
+ process.env.GATEWAY_SIGN_KEY
213
+ ? {env: 'GATEWAY_SIGN_KEY'}
214
+ : {generate: {alg: 'ES384', crv: 'P-384', use: 'sig'}}
215
+ ) as IConfig['sign'];
216
+ this.#config.encrypt ||= (
217
+ process.env.GATEWAY_ENCRYPT_KEY
218
+ ? {env: 'GATEWAY_ENCRYPT_KEY'}
219
+ : {generate: {alg: 'ECDH-ES+A256KW', crv: 'P-384', use: 'enc'}}
220
+ ) as IConfig['encrypt'];
197
221
  this.#errorFields = Object.entries({
198
222
  type: true,
199
223
  message: true,
@@ -210,6 +234,7 @@ export default class Gateway extends Internal implements IGateway {
210
234
  this.#resolution = resolution!;
211
235
  this.#rpcClient = remote!;
212
236
  this.#local = local!;
237
+ this.#manifest = manifest;
213
238
  }
214
239
 
215
240
  protected config(): object {
@@ -336,6 +361,7 @@ export default class Gateway extends Internal implements IGateway {
336
361
  : `/${value.basePath ?? 'rpc'}/${method.split('.').join('/')}`,
337
362
  config: {
338
363
  auth: value.auth ?? 'jwt',
364
+ methodName: method,
339
365
  },
340
366
  schema: Type && {
341
367
  ...('body' in value
@@ -539,11 +565,34 @@ export default class Gateway extends Internal implements IGateway {
539
565
  strict: false,
540
566
  },
541
567
  },
568
+ }).withTypeProvider<TypeBoxTypeProvider>();
569
+
570
+ this.#server.setValidatorCompiler(({schema}: {schema: TSchema}) => {
571
+ return (data: unknown) => {
572
+ // 1. Runtime validation check
573
+ if (!Value.Check(schema, data)) {
574
+ const errors = [...Value.Errors(schema, data)];
575
+ return {
576
+ error: new Error(
577
+ `Validation failed: ${errors.map(e => e.message).join(', ')}`,
578
+ ),
579
+ };
580
+ }
581
+
582
+ // 2. Transform/Decode the validated string into a Date object
583
+ try {
584
+ const decoded = Value.Decode(schema, data);
585
+ return {value: decoded};
586
+ } catch (error) {
587
+ return {error};
588
+ }
589
+ };
542
590
  });
591
+
543
592
  this.#server.setErrorHandler(
544
593
  (error: Record<string, unknown>, request: FastifyRequest, reply: FastifyReply) => {
545
594
  request.log.error({err: error}, 'gateway unhandled error');
546
- return reply.status(500).send({
595
+ return reply.status((error as {statusCode?: number}).statusCode || 500).send({
547
596
  jsonrpc: '2.0',
548
597
  id: (request.body as {id?: unknown})?.id,
549
598
  error: this._formatError(error),
@@ -556,6 +605,10 @@ export default class Gateway extends Internal implements IGateway {
556
605
  this.#rpcClient.verify(token, options),
557
606
  errors: this.#errors,
558
607
  audience: this.#config.jwt.audience,
608
+ authorize: this.#config.authorize,
609
+ local: this.#local,
610
+ methodId: methodId,
611
+ methodParts: methodParts,
559
612
  });
560
613
  if (this.#config.cors)
561
614
  await this.#server.register((await import('./cors.ts')).default, this.#config.cors);
@@ -584,6 +637,13 @@ export default class Gateway extends Internal implements IGateway {
584
637
  port: this.#config.port,
585
638
  host: this.#config.host,
586
639
  });
640
+ // Publish the effective port to the manifest so other platforms
641
+ // (e.g. browser backend adapter) can discover it.
642
+ // The manifest proxy automatically resolves any pending deferred.
643
+ const address = this.#server.server.address();
644
+ if (this.#manifest && address && typeof address === 'object') {
645
+ this.#manifest.gatewayPort = address.port;
646
+ }
587
647
  return this;
588
648
  }
589
649