@feasibleone/blong-gogo 1.20.0 → 1.22.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,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.22.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.21.0...blong-gogo-v1.22.0) (2026-05-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * intents ([5b238b0](https://github.com/feasibleone/blong/commit/5b238b064be7e20a051e963f23762012ce96bcb9))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * improve integration test coverage for keycloak, S3, and vault adapters ([#149](https://github.com/feasibleone/blong/issues/149)) ([453c9ac](https://github.com/feasibleone/blong/commit/453c9ac17bdde48c526d92e465aeeb6a6fd816bb))
14
+ * integration tests coverage report ([ce3ae79](https://github.com/feasibleone/blong/commit/ce3ae79e8056fab2d09d5f0779f24997773ba3c8))
15
+ * integration tests coverage report ([4215fc1](https://github.com/feasibleone/blong/commit/4215fc1a8620e3cf36d748f55aabb0c85917d2f2))
16
+ * integration tests coverage report ([bb8d3c7](https://github.com/feasibleone/blong/commit/bb8d3c7b0eb64c6be9d90d70ac1469c065139f31))
17
+
18
+ ## [1.21.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.20.0...blong-gogo-v1.21.0) (2026-05-12)
19
+
20
+
21
+ ### Features
22
+
23
+ * **blong-int-adapter:** expand mysql/mongodb/kafka integration test coverage ([#143](https://github.com/feasibleone/blong/issues/143)) ([b4bb9dd](https://github.com/feasibleone/blong/commit/b4bb9dd130ed9adcb6f4f28d57534f69b174321f))
24
+
3
25
  ## [1.20.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.19.1...blong-gogo-v1.20.0) (2026-05-11)
4
26
 
5
27
 
package/bin/blong-dev.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  #!/usr/bin/env -S node --watch --conditions=development --inspect
2
2
 
3
3
  import minimist from 'minimist';
4
+ import {existsSync} from 'node:fs';
5
+ import {resolve} from 'node:path';
4
6
  import {autoRun} from '../src/runServer.ts';
5
7
 
6
8
  const argv: {_: string[]} = minimist(process.argv.slice(2));
7
9
 
8
- await autoRun({cwd: process.cwd(), target: argv._[0]});
10
+ // The first positional arg is an optional file/folder target; the rest are intents.
11
+ const [maybeTarget, ...rest] = argv._;
12
+ const target = maybeTarget && existsSync(resolve(maybeTarget)) ? maybeTarget : undefined;
13
+ const intents = target ? rest : argv._;
14
+
15
+ await autoRun({cwd: process.cwd(), target, intents});
package/bin/blong.ts CHANGED
@@ -1,8 +1,15 @@
1
1
  #!/usr/bin/env -S node
2
2
 
3
3
  import minimist from 'minimist';
4
+ import {existsSync} from 'node:fs';
5
+ import {resolve} from 'node:path';
4
6
  import {autoRun} from '../src/runServer.ts';
5
7
 
6
8
  const argv: {_: string[]} = minimist(process.argv.slice(2));
7
9
 
8
- await autoRun({cwd: process.cwd(), target: argv._[0]});
10
+ // The first positional arg is an optional file/folder target; the rest are intents.
11
+ const [maybeTarget, ...rest] = argv._;
12
+ const target = maybeTarget && existsSync(resolve(maybeTarget)) ? maybeTarget : undefined;
13
+ const intents = target ? rest : argv._;
14
+
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.20.0",
3
+ "version": "1.22.0",
4
4
  "repository": {
5
5
  "url": "git+https://github.com/feasibleone/blong.git"
6
6
  },
@@ -84,6 +84,7 @@
84
84
  "build": "true",
85
85
  "ci-lint": "tsc --noEmit",
86
86
  "ci-publish": "node ../../common/scripts/install-run-rush-pnpm.js publish --access public --provenance",
87
- "ci-unit": "tap src/ConfigRuntime.test.ts src/lib.test.ts --allow-incomplete-coverage"
87
+ "ci-unit": "../common/run-coverage.sh",
88
+ "ci-coverage": "../common/run-coverage.sh report"
88
89
  }
89
90
  }
package/src/Watch.ts CHANGED
@@ -294,7 +294,10 @@ export default class Watch extends Internal implements IWatch {
294
294
  ? (await import(this.#config.enabled ? filename + '?' + Date.now() : filename))
295
295
  .default
296
296
  : ((await directory![filename]()) as {default: unknown}).default;
297
- if (!item) this.log?.error?.('Error loading ' + filename);
297
+ if (!item) {
298
+ this.log?.error?.('Error loading ' + filename);
299
+ continue;
300
+ }
298
301
  const expectedName = this.#platform.basename(
299
302
  filename,
300
303
  this.#platform.extname(filename),
@@ -249,7 +249,7 @@ export default adapter<IConfig>(({utError}) => {
249
249
 
250
250
  case 'update':
251
251
  case 'edit': {
252
- const {id: userId, ...updateData} = handleParams;
252
+ const {id: userId, realm: _realm, ...updateData} = handleParams;
253
253
  if (!userId) throw _errors['keycloak.missingKey']({key: 'id'});
254
254
  return await client.users.update({id: userId as string}, updateData);
255
255
  }
@@ -368,7 +368,7 @@ export default adapter<IConfig>(({utError}) => {
368
368
 
369
369
  case 'update':
370
370
  case 'edit': {
371
- const {id: groupId, ...updateData} = handleParams;
371
+ const {id: groupId, realm: _realm, ...updateData} = handleParams;
372
372
  if (!groupId) throw _errors['keycloak.missingKey']({key: 'id'});
373
373
  return await client.groups.update({id: groupId as string}, updateData);
374
374
  }
@@ -483,6 +483,7 @@ export default adapter<IConfig>(({utError}) => {
483
483
  const {
484
484
  roleName: updateRoleName,
485
485
  clientUuid: updateClientUuid,
486
+ realm: _realm,
486
487
  ...updateData
487
488
  } = handleParams;
488
489
  if (!updateRoleName)
@@ -499,7 +500,7 @@ export default adapter<IConfig>(({utError}) => {
499
500
  } else {
500
501
  return await client.roles.updateByName(
501
502
  {name: updateRoleName as string},
502
- updateData,
503
+ {name: updateRoleName as string, ...updateData},
503
504
  );
504
505
  }
505
506
  }
@@ -614,7 +615,11 @@ export default adapter<IConfig>(({utError}) => {
614
615
 
615
616
  case 'update':
616
617
  case 'edit': {
617
- const {id: clientUpdateId, ...clientUpdateData} = handleParams;
618
+ const {
619
+ id: clientUpdateId,
620
+ realm: _realm,
621
+ ...clientUpdateData
622
+ } = handleParams;
618
623
  if (!clientUpdateId) throw _errors['keycloak.missingKey']({key: 'id'});
619
624
  return await client.clients.update(
620
625
  {id: clientUpdateId as string},
@@ -762,6 +767,8 @@ export default adapter<IConfig>(({utError}) => {
762
767
  throw _errors['keycloak.invalid']();
763
768
  }
764
769
  } catch (error: unknown) {
770
+ // Re-throw already-typed blong errors without wrapping them
771
+ if (typeof (error as {type?: string}).type === 'string') throw error;
765
772
  const keycloakError = error as {
766
773
  response?: {status?: number; data?: {error?: string}};
767
774
  };
package/src/jose.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert';
2
- import {test} from 'node:test';
2
+ import {test} from 'tap';
3
3
 
4
4
  import jose from './jose.ts';
5
5
 
package/src/jose.ts CHANGED
@@ -269,7 +269,7 @@ export default async function jose({sign, encrypt}: {sign?: KeySpec; encrypt?: K
269
269
  ? signEncrypt(
270
270
  msg,
271
271
  mlsk,
272
- await importKey(key),
272
+ await importKey(await exportKey(key as JWK)),
273
273
  protectedHeader!,
274
274
  unprotectedHeader!,
275
275
  options,
@@ -278,7 +278,7 @@ export default async function jose({sign, encrypt}: {sign?: KeySpec; encrypt?: K
278
278
  decryptVerify: async (
279
279
  msg: Parameters<typeof decryptVerify>[0],
280
280
  key: Parameters<typeof importKey>[0],
281
- ) => (mlek ? decryptVerify(msg, await importKey(key), mlek!) : msg),
281
+ ) => (mlek ? decryptVerify(msg, await importKey(await exportKey(key as JWK)), mlek!) : msg),
282
282
  decrypt: (msg, options) =>
283
283
  mlek ? decrypt(msg, mlek!, options as {complete?: unknown} | undefined) : msg,
284
284
  verify: async (msg, key) => verify(msg, await importKey(key)),
package/src/loadServer.ts CHANGED
@@ -28,6 +28,12 @@ const loadConfig = async (parentConfig: string | object) => {
28
28
  return {loadedConfig, configRuntime};
29
29
  };
30
30
 
31
+ // Parse CLI intents: first positional arg may be a file/folder target — exclude it from intents.
32
+ const allPositional = minimist(process.argv.slice(2))._ as string[];
33
+ const [maybeTarget, ...rest] = allPositional;
34
+ const targetIsFile = Boolean(maybeTarget && existsSync(resolve(maybeTarget)));
35
+ const cliIntents = targetIsFile ? rest : allPositional;
36
+
31
37
  export default load.bind(null, {
32
38
  platform: 'server',
33
39
  readdir: async (path: string) => readdir(path, {withFileTypes: true}),
@@ -46,5 +52,5 @@ export default load.bind(null, {
46
52
  statSync,
47
53
  watch,
48
54
  timing: timing(hrtime),
49
- configs: ['server', ...minimist(process.argv.slice(2))._],
55
+ configs: ['server', ...cliIntents],
50
56
  });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Unit tests for CLI intent parsing logic in runServer.ts.
3
+ *
4
+ * These tests verify:
5
+ * - autoRun correctly separates file-path targets from intents
6
+ * - runPlatform uses DEFAULT_INTENTS when no intents are provided
7
+ * - DEFAULT_INTENTS matches the expected set
8
+ */
9
+
10
+ import {test} from 'tap';
11
+
12
+ import {DEFAULT_INTENTS} from './runServer.ts';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // DEFAULT_INTENTS
16
+ // ---------------------------------------------------------------------------
17
+
18
+ test('DEFAULT_INTENTS contains the three baseline intents', async t => {
19
+ t.same([...DEFAULT_INTENTS], ['microservice', 'integration', 'dev']);
20
+ });
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Intent extraction helper — replicated inline to avoid FS side-effects
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Mirrors the logic in bin/blong.ts:
28
+ * - first element is the target when existsSync returns true
29
+ * - remaining elements (or all elements when no target) are intents
30
+ */
31
+ function extractIntents(
32
+ positional: string[],
33
+ fileExists: (path: string) => boolean,
34
+ ): {target: string | undefined; intents: string[]} {
35
+ const [maybeTarget, ...rest] = positional;
36
+ const target = maybeTarget && fileExists(maybeTarget) ? maybeTarget : undefined;
37
+ const intents = target ? rest : positional;
38
+ return {target, intents};
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // extractIntents — file-path as first positional
43
+ // ---------------------------------------------------------------------------
44
+
45
+ test('extractIntents — recognises existing file as target', async t => {
46
+ const {target, intents} = extractIntents(['./server.ts', 'integration'], () => true);
47
+ t.equal(target, './server.ts');
48
+ t.same(intents, ['integration']);
49
+ });
50
+
51
+ test('extractIntents — non-existent path is treated as an intent', async t => {
52
+ const {target, intents} = extractIntents(['integration', 'dev'], () => false);
53
+ t.equal(target, undefined);
54
+ t.same(intents, ['integration', 'dev']);
55
+ });
56
+
57
+ test('extractIntents — empty args yield no target and no intents', async t => {
58
+ const {target, intents} = extractIntents([], () => false);
59
+ t.equal(target, undefined);
60
+ t.same(intents, []);
61
+ });
62
+
63
+ test('extractIntents — single file target, no intents', async t => {
64
+ const {target, intents} = extractIntents(['/abs/path/server.ts'], () => true);
65
+ t.equal(target, '/abs/path/server.ts');
66
+ t.same(intents, []);
67
+ });
68
+
69
+ test('extractIntents — multiple intents with no target', async t => {
70
+ const {target, intents} = extractIntents(
71
+ ['integration', 'microservice', 'debug'],
72
+ () => false,
73
+ );
74
+ t.equal(target, undefined);
75
+ t.same(intents, ['integration', 'microservice', 'debug']);
76
+ });
77
+
78
+ test('extractIntents — multiple intents after a valid target', async t => {
79
+ const {target, intents} = extractIntents(
80
+ ['./index.ts', 'integration', 'debug'],
81
+ () => true,
82
+ );
83
+ t.equal(target, './index.ts');
84
+ t.same(intents, ['integration', 'debug']);
85
+ });
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Intent resolution — default fallback when none provided
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Mirrors the intent-resolution logic in autoRun:
93
+ * cliIntents.length > 0 → use cliIntents
94
+ * otherwise → fall back to DEFAULT_INTENTS
95
+ */
96
+ function resolveIntents(cliIntents: string[]): string[] {
97
+ return cliIntents.length > 0 ? cliIntents : [...DEFAULT_INTENTS];
98
+ }
99
+
100
+ test('resolveIntents — empty CLI intents fall back to defaults', async t => {
101
+ t.same(resolveIntents([]), [...DEFAULT_INTENTS]);
102
+ });
103
+
104
+ test('resolveIntents — provided intents are used as-is', async t => {
105
+ t.same(resolveIntents(['integration']), ['integration']);
106
+ });
107
+
108
+ test('resolveIntents — custom intent is passed through', async t => {
109
+ t.same(resolveIntents(['db']), ['db']);
110
+ });
111
+
112
+ test('resolveIntents — multiple intents preserved in order', async t => {
113
+ t.same(resolveIntents(['dev', 'debug']), ['dev', 'debug']);
114
+ });
package/src/runServer.ts CHANGED
@@ -4,15 +4,21 @@ import {basename, resolve} from 'node:path';
4
4
  import {analyzeFolder, synthesizeServerFromHandlers} from './folderAnalysis.ts';
5
5
  import load from './loadServer.ts';
6
6
 
7
+ /** Default intents applied when none are provided on the CLI. */
8
+ export const DEFAULT_INTENTS = ['microservice', 'integration', 'dev'] as const;
9
+
7
10
  /**
8
11
  * Runs the standard platform lifecycle: start → test → (CI) stop.
12
+ *
13
+ * @param intents - Active intents that control which config blocks and layers are activated.
14
+ * Defaults to {@link DEFAULT_INTENTS} when omitted.
9
15
  */
10
- export async function runPlatform(serverDef: SolutionFactory, name: string): Promise<void> {
11
- const platform = await load(serverDef as unknown as Parameters<typeof load>[0], name, name, [
12
- 'microservice',
13
- 'integration',
14
- 'dev',
15
- ]);
16
+ export async function runPlatform(
17
+ serverDef: SolutionFactory,
18
+ name: string,
19
+ intents: string[] = [...DEFAULT_INTENTS],
20
+ ): Promise<void> {
21
+ const platform = await load(serverDef as unknown as Parameters<typeof load>[0], name, name, intents);
16
22
  await platform.start!({});
17
23
  await platform.test!(undefined);
18
24
  if (process.env.CI) await platform.stop!();
@@ -28,9 +34,19 @@ export async function runPlatform(serverDef: SolutionFactory, name: string): Pro
28
34
  * 4. `server.ts` alone — server-only suite or realm.
29
35
  * 5. Folder contains handler files — synthesize a server suite on the fly.
30
36
  * 6. Throws if nothing matched.
37
+ *
38
+ * @param options.intents - Active intents from the CLI (positional args after the optional target
39
+ * path). When empty, {@link DEFAULT_INTENTS} are used so that a plain `blong` invocation works
40
+ * out of the box.
31
41
  */
32
- export async function autoRun(options: {cwd: string; target?: string}): Promise<void> {
33
- const {cwd, target} = options;
42
+ export async function autoRun(options: {
43
+ cwd: string;
44
+ target?: string;
45
+ intents?: string[];
46
+ }): Promise<void> {
47
+ const {cwd, target, intents: cliIntents} = options;
48
+ // Use CLI-supplied intents; fall back to defaults when the user passed none.
49
+ const intents = cliIntents && cliIntents.length > 0 ? cliIntents : [...DEFAULT_INTENTS];
34
50
 
35
51
  if (target && existsSync(target)) {
36
52
  (await import(target)).default(load);
@@ -48,19 +64,19 @@ export async function autoRun(options: {cwd: string; target?: string}): Promise<
48
64
  const {default: serverDef} = await import(serverFile);
49
65
  const {default: browserDef} = await import(browserFile);
50
66
  const platforms: Awaited<ReturnType<typeof load>>[] = await Promise.all([
51
- load(serverDef, name, name, ['microservice', 'integration', 'dev']),
52
- load(browserDef, name, name, ['microservice', 'integration', 'dev']),
67
+ load(serverDef, name, name, intents),
68
+ load(browserDef, name, name, intents),
53
69
  ]);
54
70
  for (const platform of platforms) await platform.start({});
55
71
  await platforms[1].test!(undefined);
56
72
  if (process.env.CI) for (const platform of platforms) await platform.stop();
57
73
  } else if (existsSync(serverFile)) {
58
74
  const {default: serverDef} = await import(serverFile);
59
- await runPlatform(serverDef, name);
75
+ await runPlatform(serverDef, name, intents);
60
76
  } else {
61
77
  const analysis = await analyzeFolder(cwd);
62
78
  if (analysis.kind === 'handlers' || analysis.kind === 'mixed') {
63
- await runPlatform(await synthesizeServerFromHandlers(cwd, analysis), name);
79
+ await runPlatform(await synthesizeServerFromHandlers(cwd, analysis), name, intents);
64
80
  } else {
65
81
  throw new Error(
66
82
  `No entry point found in ${cwd}. ` +