@feasibleone/blong-gogo 1.21.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 +15 -0
- package/bin/blong-dev.ts +8 -1
- package/bin/blong.ts +8 -1
- package/package.json +3 -2
- package/src/adapter/server/keycloak.ts +11 -4
- package/src/jose.test.ts +1 -1
- package/src/jose.ts +2 -2
- package/src/loadServer.ts +7 -1
- package/src/runServer.test.ts +114 -0
- package/src/runServer.ts +28 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
3
18
|
## [1.21.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.20.0...blong-gogo-v1.21.0) (2026-05-12)
|
|
4
19
|
|
|
5
20
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
87
|
+
"ci-unit": "../common/run-coverage.sh",
|
|
88
|
+
"ci-coverage": "../common/run-coverage.sh report"
|
|
88
89
|
}
|
|
89
90
|
}
|
|
@@ -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 {
|
|
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
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', ...
|
|
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(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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: {
|
|
33
|
-
|
|
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,
|
|
52
|
-
load(browserDef, name, name,
|
|
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}. ` +
|