@owlmeans/api-config 0.1.18-rc.11 → 0.1.18-rc.13

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/README.md CHANGED
@@ -4,15 +4,15 @@ Shared entrypoint for advertising safe config values from server to client via a
4
4
 
5
5
  ## Overview
6
6
 
7
- - Exposes a `GET /assets/config.json` entrypoint that returns non-sensitive config fields
7
+ - Exposes a `GET /assets/config.json` entrypoint that returns package-allowlisted config fields
8
8
  - `ApiConfig` — the advertised config type (subset of `CommonConfig`)
9
9
  - `API_CONFIG` — entrypoint alias for the config endpoint
10
- - `notAdvertizedConfigKeys` / `allowedConfigRecords` — lists controlling what is/isn't exposed
10
+ - `apiConfigPlugin` / `every` — import-time allowlist helpers for package-owned client config
11
11
 
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/api-config
15
+ bun add @owlmeans/api-config@^0.1.18-rc.11
16
16
  ```
17
17
 
18
18
  ## Usage
@@ -28,12 +28,29 @@ import type { ApiConfig } from '@owlmeans/api-config'
28
28
 
29
29
  ### `ApiConfig`
30
30
 
31
- Subset of `CommonConfig` safe to expose to clients (no db credentials, secrets, etc.).
31
+ Partial public config assembled only from imported packages' allowlist plugins. Unregistered
32
+ server values — including databases, queues and secrets — are never returned.
32
33
 
33
34
  ### `API_CONFIG`
34
35
 
35
36
  Entrypoint alias `'api-config:advertise'` used to register/call the config endpoint.
36
37
 
38
+ ### `apiConfigPlugin`
39
+
40
+ Call this once at module scope in the package that owns a client-facing configuration field:
41
+
42
+ ```typescript
43
+ import { apiConfigPlugin, every } from '@owlmeans/api-config'
44
+
45
+ apiConfigPlugin({
46
+ allow: { integrations: every(true) },
47
+ deny: { integrations: every({ token: true }) },
48
+ })
49
+ ```
50
+
51
+ The `deny` selector removes nested values after the allowlist selection. Prefer a precise nested
52
+ allowlist to `true`; `true` is appropriate only for data that is public at every depth.
53
+
37
54
  ### `entrypoints`
38
55
 
39
56
  Array of route definitions for the config advertisement endpoint.
@@ -51,7 +68,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
51
68
  your project's skill store (`.agents/skills/`):
52
69
 
53
70
  ```sh
54
- npx @owlmeans/agent-skills
71
+ npx @owlmeans/agent-skills@^0.1.18-rc.13
55
72
  ```
56
73
 
57
74
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/api-config",
4
- "version": "0.1.18-rc.11",
5
- "generatedAt": "2026-09-01T17:39:40.767Z",
4
+ "version": "0.1.18-rc.13",
5
+ "generatedAt": "2026-09-10T18:54:42.506Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: api-config
3
- description: How to use @owlmeans/api-config — module declarations for fetching API/service configuration at runtime. Auto-invoked when importing api-config types or modules.
3
+ description: How to use @owlmeans/api-config — the shared entrypoint declaration and import-time allowlist plugins behind the runtime config document a backend advertises and a frontend merges into its own config at boot. Auto-invoked when importing api-config, or when wiring runtime API config between a server and a client.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,25 +8,59 @@ user-invocable: false
8
8
  # @owlmeans/api-config
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/api-config": "^0.1.18-rc.11"` in `dependencies`
11
+ **Install:** `"@owlmeans/api-config": "^0.1.18-rc.13"` in `dependencies`
12
+
13
+ The contract package of a three-package flow: this one declares the endpoint and what may cross it,
14
+ `@owlmeans/api-config-server` answers it, `@owlmeans/api-config-client` fetches it and merges the
15
+ answer into the client config. Nothing here runs — it is the shared declaration all three agree on.
12
16
 
13
17
  ## Key Exports
14
18
 
15
19
  | Export | Description |
16
20
  |--------|-------------|
17
- | `ApiConfig` types | Shape of remotely-served API config |
18
- | Modules | Module declarations for the config endpoint |
19
- | Constants | Module aliases, route paths |
21
+ | `entrypoints` | The single declaration — alias `API_CONFIG`, route `/assets/config.json`, `sticky: true` so a router attaches it unconditionally |
22
+ | `API_CONFIG` | The alias (`api-config:advertise`) both sides elevate |
23
+ | `apiConfigPlugin(plugin)` | Registers one package's public config selection as its module loads |
24
+ | `every(selection, where?)` | Applies a selection to every list item or object-map value, optionally filtering items |
25
+ | `ApiConfigPlugin` | `{ allow, deny? }` — nested allowlist with an optional nested redaction selector |
26
+ | `advertisedConfig(cfg)` | Applies every imported package contract to build the endpoint document |
27
+ | `ApiConfig` | The partial public document a client merges into its local config |
20
28
 
21
29
  ## Usage
22
30
 
23
- Pair with `@owlmeans/api-config-client` (browser) or `@owlmeans/api-config-server` (Fastify) for the runtime side. The `api-config` package itself defines the shared module/route shape.
31
+ The declaration is elevated on both sides, so neither imports the other's package — they only share
32
+ this one. Add it to the entrypoint list the way any other entrypoint set is added:
24
33
 
25
34
  ```typescript
26
- import { modules as apiConfigModules } from '@owlmeans/api-config'
27
- const appModules = [...apiConfigModules, ...myModules]
35
+ import { entrypoints as apiConfigEntrypoints } from '@owlmeans/api-config'
36
+ export const appEntrypoints = [...apiConfigEntrypoints, ...myEntrypoints]
28
37
  ```
29
38
 
39
+ Most apps never do this directly: a backend built on `@owlmeans/server-app` already carries the
40
+ server side in its built-in `entrypoints`, and a client panel package already carries the client
41
+ side.
42
+
43
+ ## What may cross
44
+
45
+ The document is default-deny: a server config value is absent until the package that owns its
46
+ browser consumer registers a precise `allow` selector during import. Do not allow an ancestor with
47
+ `true` unless every descendant is public; name fields instead, and use `deny` when an otherwise
48
+ public collection carries nested credentials.
49
+
50
+ ```typescript
51
+ import { apiConfigPlugin, every } from '@owlmeans/api-config'
52
+
53
+ apiConfigPlugin({
54
+ allow: { oidc: { providers: every(true) } },
55
+ deny: { oidc: { providers: every({ secret: true, apiClientId: true }) } },
56
+ })
57
+ ```
58
+
59
+ The base contract supplies public routes, branding, login settings, selected debug flags and
60
+ frontend plugins. `@owlmeans/oidc`, `@owlmeans/flow`, `@owlmeans/i18n` and `@owlmeans/payment`
61
+ register their own client settings during import. Databases, queues, SMTP, secrets and every
62
+ unregistered config extension never cross the unauthenticated endpoint.
63
+
30
64
  ## Depends On
31
65
 
32
66
  - `@owlmeans/entrypoint`, `@owlmeans/route`, `@owlmeans/config`
package/build/consts.d.ts CHANGED
@@ -1,4 +1,2 @@
1
1
  export declare const API_CONFIG = "api-config:advertise";
2
- export declare const notAdvertizedConfigKeys: string[];
3
- export declare const allowedConfigRecords: string[];
4
2
  //# sourceMappingURL=consts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,UAAU,yBAAyB,CAAA;AAEhD,eAAO,MAAM,uBAAuB,UAInC,CAAA;AAED,eAAO,MAAM,oBAAoB,UAEhC,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,UAAU,yBAAyB,CAAA"}
package/build/consts.js CHANGED
@@ -1,10 +1,2 @@
1
1
  export const API_CONFIG = 'api-config:advertise';
2
- export const notAdvertizedConfigKeys = [
3
- 'dbs', 'trusted', 'ready', 'service', 'type',
4
- 'records', 'webService', 'oidc', 'storageBuckets',
5
- 'secrets',
6
- ];
7
- export const allowedConfigRecords = [
8
- 'plan', 'product', 'l10n'
9
- ];
10
2
  //# sourceMappingURL=consts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,UAAU,GAAG,sBAAsB,CAAA;AAEhD,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM;IAC5C,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,gBAAgB;IACjD,SAAS;CACV,CAAA;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,MAAM,EAAE,SAAS,EAAE,MAAM;CAC1B,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,UAAU,GAAG,sBAAsB,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=defaults.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import { AppType } from '@owlmeans/context';
2
+ import { PLUGINS } from '@owlmeans/config';
3
+ import { apiConfigPlugin, every } from './plugins.js';
4
+ const isPublicService = (value) => {
5
+ if (value == null || typeof value !== 'object') {
6
+ return false;
7
+ }
8
+ const service = value;
9
+ // sservice() copies an internal address into host/port. Keep a service only when it has an
10
+ // independently declared public address; otherwise an internal-only peer leaks through host.
11
+ return service.internalHost == null || service.host !== service.internalHost;
12
+ };
13
+ apiConfigPlugin({
14
+ allow: {
15
+ debug: { all: true, i18n: true, supervisor: true },
16
+ brand: true,
17
+ security: {
18
+ unsecure: true,
19
+ auth: { flow: true, enter: true, login: true },
20
+ },
21
+ services: every({
22
+ service: true,
23
+ type: true,
24
+ host: true,
25
+ port: true,
26
+ base: true,
27
+ home: true,
28
+ default: true,
29
+ }, isPublicService),
30
+ [PLUGINS]: every(true, value => value != null && typeof value === 'object' && value.type === AppType.Frontend),
31
+ },
32
+ });
33
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../src/defaults.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAErD,MAAM,eAAe,GAAG,CAAC,KAAc,EAAW,EAAE;IAClD,IAAI,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/C,OAAO,KAAK,CAAA;IACd,CAAC;IACD,MAAM,OAAO,GAAG,KAAmD,CAAA;IAEnE,2FAA2F;IAC3F,6FAA6F;IAC7F,OAAO,OAAO,CAAC,YAAY,IAAI,IAAI,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,YAAY,CAAA;AAC9E,CAAC,CAAA;AAED,eAAe,CAAC;IACd,KAAK,EAAE;QACL,KAAK,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE;QAClD,KAAK,EAAE,IAAI;QACX,QAAQ,EAAE;YACR,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;SAC/C;QACD,QAAQ,EAAE,KAAK,CAAC;YACd,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,IAAI;SACd,EAAE,eAAe,CAAC;QACnB,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAC7B,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAK,KAA4B,CAAC,IAAI,KAAK,OAAO,CAAC,QAAQ,CACtG;KACF;CACF,CAAC,CAAA"}
package/build/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export type * from './types.js';
2
2
  export * from './consts.js';
3
3
  export * from './entrypoints.js';
4
+ export * from './plugins.js';
5
+ export * from './defaults.js';
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA"}
package/build/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from './consts.js';
2
2
  export * from './entrypoints.js';
3
+ export * from './plugins.js';
4
+ export * from './defaults.js';
3
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA"}
@@ -0,0 +1,31 @@
1
+ /** A declarative selection of config values that may cross into a browser. */
2
+ export type ConfigSelection = true | ConfigSelectionMap | EveryConfigValue;
3
+ export interface ConfigSelectionMap {
4
+ readonly [key: string]: ConfigSelection | undefined;
5
+ }
6
+ /** Apply a selection to every item in an array or every value in an object map. */
7
+ export interface EveryConfigValue {
8
+ readonly every: ConfigSelection;
9
+ readonly where?: (value: unknown) => boolean;
10
+ }
11
+ /** A package-owned contribution to the public runtime-config document. */
12
+ export interface ApiConfigPlugin {
13
+ /** Fields this package intentionally makes public. */
14
+ readonly allow: ConfigSelection;
15
+ /** Nested fields to remove after selection, even when an ancestor is allowed. */
16
+ readonly deny?: ConfigSelection;
17
+ }
18
+ /** Build a selector for lists and string-keyed maps. */
19
+ export declare const every: (selection: ConfigSelection, where?: (value: unknown) => boolean) => EveryConfigValue;
20
+ /**
21
+ * Register a package's public config contract during module import.
22
+ *
23
+ * The registry is intentionally additive: importing a package can only expose the fields it
24
+ * names, while a package's `deny` selector can still remove sensitive nested values.
25
+ */
26
+ export declare const apiConfigPlugin: (plugin: ApiConfigPlugin) => void;
27
+ /** Apply one package contract without registering it; useful for focused contract tests. */
28
+ export declare const selectApiConfig: (config: Record<string, unknown>, plugin: ApiConfigPlugin) => Record<string, unknown>;
29
+ /** Build the document the unauthenticated config endpoint may return. */
30
+ export declare const advertisedConfig: <C extends object>(config: C) => Record<string, unknown>;
31
+ //# sourceMappingURL=plugins.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugins.d.ts","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,kBAAkB,GAAG,gBAAgB,CAAA;AAE1E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAAA;CACpD;AAED,mFAAmF;AACnF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAA;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAA;CAC7C;AAED,0EAA0E;AAC1E,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAA;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,eAAe,CAAA;CAChC;AAaD,wDAAwD;AACxD,eAAO,MAAM,KAAK,cAAe,eAAe,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,KAAG,gBAC3B,CAAA;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,eAAe,WAAY,eAAe,KAAG,IAEzD,CAAA;AAoGD,4FAA4F;AAC5F,eAAO,MAAM,eAAe,WAAY,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,eAAe,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAQhH,CAAA;AAED,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,MAAM,UAAU,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAIlF,CAAA"}
@@ -0,0 +1,121 @@
1
+ const plugins = [];
2
+ const OMIT = Symbol('api-config:omit');
3
+ const isRecord = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
4
+ const isEvery = (selection) => selection !== true && 'every' in selection;
5
+ /** Build a selector for lists and string-keyed maps. */
6
+ export const every = (selection, where) => ({ every: selection, ...(where != null ? { where } : {}) });
7
+ /**
8
+ * Register a package's public config contract during module import.
9
+ *
10
+ * The registry is intentionally additive: importing a package can only expose the fields it
11
+ * names, while a package's `deny` selector can still remove sensitive nested values.
12
+ */
13
+ export const apiConfigPlugin = (plugin) => {
14
+ plugins.push(plugin);
15
+ };
16
+ const select = (value, selection) => {
17
+ if (value === undefined) {
18
+ return OMIT;
19
+ }
20
+ if (selection === true) {
21
+ return value;
22
+ }
23
+ if (isEvery(selection)) {
24
+ if (Array.isArray(value)) {
25
+ return value.flatMap(item => {
26
+ if (selection.where?.(item) === false) {
27
+ return [];
28
+ }
29
+ const selected = select(item, selection.every);
30
+ return selected === OMIT ? [] : [selected];
31
+ });
32
+ }
33
+ if (isRecord(value)) {
34
+ return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => {
35
+ if (selection.where?.(item) === false) {
36
+ return [];
37
+ }
38
+ const selected = select(item, selection.every);
39
+ return selected === OMIT ? [] : [[key, selected]];
40
+ }));
41
+ }
42
+ return OMIT;
43
+ }
44
+ if (!isRecord(value)) {
45
+ return OMIT;
46
+ }
47
+ return Object.fromEntries(Object.entries(selection).flatMap(([key, nested]) => {
48
+ if (nested == null) {
49
+ return [];
50
+ }
51
+ const selected = select(value[key], nested);
52
+ return selected === OMIT ? [] : [[key, selected]];
53
+ }));
54
+ };
55
+ const redact = (value, selection) => {
56
+ if (selection === true) {
57
+ return OMIT;
58
+ }
59
+ if (isEvery(selection)) {
60
+ if (Array.isArray(value)) {
61
+ return value.flatMap(item => {
62
+ if (selection.where?.(item) === false) {
63
+ return [item];
64
+ }
65
+ const redacted = redact(item, selection.every);
66
+ return redacted === OMIT ? [] : [redacted];
67
+ });
68
+ }
69
+ if (isRecord(value)) {
70
+ return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => {
71
+ if (selection.where?.(item) === false) {
72
+ return [[key, item]];
73
+ }
74
+ const redacted = redact(item, selection.every);
75
+ return redacted === OMIT ? [] : [[key, redacted]];
76
+ }));
77
+ }
78
+ return value;
79
+ }
80
+ if (Array.isArray(value)) {
81
+ return value.map(item => redact(item, selection)).filter(item => item !== OMIT);
82
+ }
83
+ if (!isRecord(value)) {
84
+ return value;
85
+ }
86
+ return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => {
87
+ const nested = selection[key];
88
+ if (nested == null) {
89
+ return [[key, item]];
90
+ }
91
+ const redacted = redact(item, nested);
92
+ return redacted === OMIT ? [] : [[key, redacted]];
93
+ }));
94
+ };
95
+ const merge = (target, source) => {
96
+ Object.entries(source).forEach(([key, value]) => {
97
+ const current = target[key];
98
+ if (Array.isArray(current) && Array.isArray(value)) {
99
+ target[key] = [...current, ...value];
100
+ }
101
+ else if (isRecord(current) && isRecord(value)) {
102
+ target[key] = merge({ ...current }, value);
103
+ }
104
+ else {
105
+ target[key] = value;
106
+ }
107
+ });
108
+ return target;
109
+ };
110
+ /** Apply one package contract without registering it; useful for focused contract tests. */
111
+ export const selectApiConfig = (config, plugin) => {
112
+ const selected = select(config, plugin.allow);
113
+ if (!isRecord(selected)) {
114
+ return {};
115
+ }
116
+ const redacted = plugin.deny == null ? selected : redact(selected, plugin.deny);
117
+ return isRecord(redacted) ? redacted : {};
118
+ };
119
+ /** Build the document the unauthenticated config endpoint may return. */
120
+ export const advertisedConfig = (config) => plugins.reduce((result, plugin) => merge(result, selectApiConfig(config, plugin)), {});
121
+ //# sourceMappingURL=plugins.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugins.js","sourceRoot":"","sources":["../src/plugins.ts"],"names":[],"mappings":"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"}
package/build/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { CommonConfig } from '@owlmeans/config';
2
- export interface ApiConfig extends Omit<CommonConfig, 'dbs' | 'trusted' | 'ready' | 'service' | 'type'> {
2
+ /** The public subset registered by packages through apiConfigPlugin(). */
3
+ export interface ApiConfig extends Partial<CommonConfig> {
3
4
  }
4
5
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAEpD,MAAM,WAAW,SAAU,SAAQ,IAAI,CACrC,YAAY,EACZ,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CACjD;CACA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAEpD,0EAA0E;AAC1E,MAAM,WAAW,SAAU,SAAQ,OAAO,CAAC,YAAY,CAAC;CACvD"}
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@owlmeans/api-config",
3
- "version": "0.1.18-rc.11",
3
+ "version": "0.1.18-rc.13",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsc -b",
8
+ "test": "bun test ./tests",
8
9
  "dev": "sleep 6 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
10
  "watch": "tsc -b -w --preserveWatchOutput --pretty"
10
11
  },
@@ -21,9 +22,10 @@
21
22
  }
22
23
  },
23
24
  "dependencies": {
24
- "@owlmeans/config": "^0.1.18-rc.11",
25
- "@owlmeans/entrypoint": "^0.1.18-rc.10",
26
- "@owlmeans/route": "^0.1.18-rc.8"
25
+ "@owlmeans/config": "^0.1.18-rc.13",
26
+ "@owlmeans/context": "^0.1.18-rc.9",
27
+ "@owlmeans/entrypoint": "^0.1.18-rc.12",
28
+ "@owlmeans/route": "^0.1.18-rc.10"
27
29
  },
28
30
  "devDependencies": {
29
31
  "@owlmeans/dep-config": "workspace:*",
package/src/consts.ts CHANGED
@@ -1,12 +1,2 @@
1
1
 
2
2
  export const API_CONFIG = 'api-config:advertise'
3
-
4
- export const notAdvertizedConfigKeys = [
5
- 'dbs', 'trusted', 'ready', 'service', 'type',
6
- 'records', 'webService', 'oidc', 'storageBuckets',
7
- 'secrets',
8
- ]
9
-
10
- export const allowedConfigRecords = [
11
- 'plan', 'product', 'l10n'
12
- ]
@@ -0,0 +1,37 @@
1
+ import { AppType } from '@owlmeans/context'
2
+ import { PLUGINS } from '@owlmeans/config'
3
+ import { apiConfigPlugin, every } from './plugins.js'
4
+
5
+ const isPublicService = (value: unknown): boolean => {
6
+ if (value == null || typeof value !== 'object') {
7
+ return false
8
+ }
9
+ const service = value as { host?: unknown, internalHost?: unknown }
10
+
11
+ // sservice() copies an internal address into host/port. Keep a service only when it has an
12
+ // independently declared public address; otherwise an internal-only peer leaks through host.
13
+ return service.internalHost == null || service.host !== service.internalHost
14
+ }
15
+
16
+ apiConfigPlugin({
17
+ allow: {
18
+ debug: { all: true, i18n: true, supervisor: true },
19
+ brand: true,
20
+ security: {
21
+ unsecure: true,
22
+ auth: { flow: true, enter: true, login: true },
23
+ },
24
+ services: every({
25
+ service: true,
26
+ type: true,
27
+ host: true,
28
+ port: true,
29
+ base: true,
30
+ home: true,
31
+ default: true,
32
+ }, isPublicService),
33
+ [PLUGINS]: every(true, value =>
34
+ value != null && typeof value === 'object' && (value as { type?: AppType }).type === AppType.Frontend
35
+ ),
36
+ },
37
+ })
package/src/index.ts CHANGED
@@ -3,3 +3,5 @@ export type * from './types.js'
3
3
 
4
4
  export * from './consts.js'
5
5
  export * from './entrypoints.js'
6
+ export * from './plugins.js'
7
+ export * from './defaults.js'
package/src/plugins.ts ADDED
@@ -0,0 +1,161 @@
1
+ /** A declarative selection of config values that may cross into a browser. */
2
+ export type ConfigSelection = true | ConfigSelectionMap | EveryConfigValue
3
+
4
+ export interface ConfigSelectionMap {
5
+ readonly [key: string]: ConfigSelection | undefined
6
+ }
7
+
8
+ /** Apply a selection to every item in an array or every value in an object map. */
9
+ export interface EveryConfigValue {
10
+ readonly every: ConfigSelection
11
+ readonly where?: (value: unknown) => boolean
12
+ }
13
+
14
+ /** A package-owned contribution to the public runtime-config document. */
15
+ export interface ApiConfigPlugin {
16
+ /** Fields this package intentionally makes public. */
17
+ readonly allow: ConfigSelection
18
+ /** Nested fields to remove after selection, even when an ancestor is allowed. */
19
+ readonly deny?: ConfigSelection
20
+ }
21
+
22
+ const plugins: ApiConfigPlugin[] = []
23
+ const OMIT = Symbol('api-config:omit')
24
+
25
+ type SelectionResult = unknown | typeof OMIT
26
+
27
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
28
+ value != null && typeof value === 'object' && !Array.isArray(value)
29
+
30
+ const isEvery = (selection: ConfigSelection): selection is EveryConfigValue =>
31
+ selection !== true && 'every' in selection
32
+
33
+ /** Build a selector for lists and string-keyed maps. */
34
+ export const every = (selection: ConfigSelection, where?: (value: unknown) => boolean): EveryConfigValue =>
35
+ ({ every: selection, ...(where != null ? { where } : {}) })
36
+
37
+ /**
38
+ * Register a package's public config contract during module import.
39
+ *
40
+ * The registry is intentionally additive: importing a package can only expose the fields it
41
+ * names, while a package's `deny` selector can still remove sensitive nested values.
42
+ */
43
+ export const apiConfigPlugin = (plugin: ApiConfigPlugin): void => {
44
+ plugins.push(plugin)
45
+ }
46
+
47
+ const select = (value: unknown, selection: ConfigSelection): SelectionResult => {
48
+ if (value === undefined) {
49
+ return OMIT
50
+ }
51
+ if (selection === true) {
52
+ return value
53
+ }
54
+ if (isEvery(selection)) {
55
+ if (Array.isArray(value)) {
56
+ return value.flatMap(item => {
57
+ if (selection.where?.(item) === false) {
58
+ return []
59
+ }
60
+ const selected = select(item, selection.every)
61
+ return selected === OMIT ? [] : [selected]
62
+ })
63
+ }
64
+ if (isRecord(value)) {
65
+ return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => {
66
+ if (selection.where?.(item) === false) {
67
+ return []
68
+ }
69
+ const selected = select(item, selection.every)
70
+ return selected === OMIT ? [] : [[key, selected]]
71
+ }))
72
+ }
73
+ return OMIT
74
+ }
75
+ if (!isRecord(value)) {
76
+ return OMIT
77
+ }
78
+
79
+ return Object.fromEntries(Object.entries(selection).flatMap(([key, nested]) => {
80
+ if (nested == null) {
81
+ return []
82
+ }
83
+ const selected = select(value[key], nested)
84
+ return selected === OMIT ? [] : [[key, selected]]
85
+ }))
86
+ }
87
+
88
+ const redact = (value: unknown, selection: ConfigSelection): SelectionResult => {
89
+ if (selection === true) {
90
+ return OMIT
91
+ }
92
+ if (isEvery(selection)) {
93
+ if (Array.isArray(value)) {
94
+ return value.flatMap(item => {
95
+ if (selection.where?.(item) === false) {
96
+ return [item]
97
+ }
98
+ const redacted = redact(item, selection.every)
99
+ return redacted === OMIT ? [] : [redacted]
100
+ })
101
+ }
102
+ if (isRecord(value)) {
103
+ return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => {
104
+ if (selection.where?.(item) === false) {
105
+ return [[key, item]]
106
+ }
107
+ const redacted = redact(item, selection.every)
108
+ return redacted === OMIT ? [] : [[key, redacted]]
109
+ }))
110
+ }
111
+ return value
112
+ }
113
+ if (Array.isArray(value)) {
114
+ return value.map(item => redact(item, selection)).filter(item => item !== OMIT)
115
+ }
116
+ if (!isRecord(value)) {
117
+ return value
118
+ }
119
+
120
+ return Object.fromEntries(Object.entries(value).flatMap(([key, item]) => {
121
+ const nested = selection[key]
122
+ if (nested == null) {
123
+ return [[key, item]]
124
+ }
125
+ const redacted = redact(item, nested)
126
+ return redacted === OMIT ? [] : [[key, redacted]]
127
+ }))
128
+ }
129
+
130
+ const merge = (target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> => {
131
+ Object.entries(source).forEach(([key, value]) => {
132
+ const current = target[key]
133
+ if (Array.isArray(current) && Array.isArray(value)) {
134
+ target[key] = [...current, ...value]
135
+ } else if (isRecord(current) && isRecord(value)) {
136
+ target[key] = merge({ ...current }, value)
137
+ } else {
138
+ target[key] = value
139
+ }
140
+ })
141
+
142
+ return target
143
+ }
144
+
145
+ /** Apply one package contract without registering it; useful for focused contract tests. */
146
+ export const selectApiConfig = (config: Record<string, unknown>, plugin: ApiConfigPlugin): Record<string, unknown> => {
147
+ const selected = select(config, plugin.allow)
148
+ if (!isRecord(selected)) {
149
+ return {}
150
+ }
151
+ const redacted = plugin.deny == null ? selected : redact(selected, plugin.deny)
152
+
153
+ return isRecord(redacted) ? redacted : {}
154
+ }
155
+
156
+ /** Build the document the unauthenticated config endpoint may return. */
157
+ export const advertisedConfig = <C extends object>(config: C): Record<string, unknown> =>
158
+ plugins.reduce(
159
+ (result, plugin) => merge(result, selectApiConfig(config as Record<string, unknown>, plugin)),
160
+ {} as Record<string, unknown>
161
+ )
package/src/types.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import type { CommonConfig } from '@owlmeans/config'
2
2
 
3
- export interface ApiConfig extends Omit<
4
- CommonConfig,
5
- 'dbs' | 'trusted' | 'ready' | 'service' | 'type'
6
- > {
3
+ /** The public subset registered by packages through apiConfigPlugin(). */
4
+ export interface ApiConfig extends Partial<CommonConfig> {
7
5
  }
@@ -0,0 +1,85 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { AppType, CONFIG_RECORD } from '@owlmeans/context'
3
+ import '@owlmeans/flow'
4
+ import '@owlmeans/i18n'
5
+ import '@owlmeans/oidc'
6
+ import '@owlmeans/payment'
7
+ import { advertisedConfig, every, selectApiConfig } from '@owlmeans/api-config'
8
+ import type { CommonConfig } from '@owlmeans/config'
9
+
10
+ describe('@owlmeans/api-config plugins', () => {
11
+ test('keeps only registered config and public nested fields', () => {
12
+ const result = advertisedConfig({
13
+ debug: { all: true, supervisor: true, serverTrace: 'private' },
14
+ brand: { name: 'Example' },
15
+ security: {
16
+ unsecure: false,
17
+ auth: { login: { title: 'Sign in' }, serverOnly: 'private' },
18
+ },
19
+ services: {
20
+ api: {
21
+ service: 'api', type: AppType.Backend, host: 'api.example.test', port: 443,
22
+ base: 'api', internalHost: 'api.cluster.local', serverOnly: 'private',
23
+ },
24
+ worker: {
25
+ service: 'worker', type: AppType.Backend, host: 'worker.cluster.local',
26
+ internalHost: 'worker.cluster.local', port: 3000,
27
+ },
28
+ },
29
+ plugins: [
30
+ { id: 'web', type: AppType.Frontend, value: 'safe' },
31
+ { id: 'server', type: AppType.Backend, value: 'private' },
32
+ ],
33
+ oidc: {
34
+ clientCookie: { interaction: { name: 'oidc', ttl: 60 } },
35
+ accountLinkingService: 'private',
36
+ providers: [
37
+ { clientId: 'public', service: 'iam', secret: 'secret', apiClientId: 'admin' },
38
+ { clientId: 'internal', service: 'iam', internal: true, secret: 'secret' },
39
+ ],
40
+ },
41
+ flowConfig: { defaultFlow: 'signin', services: { auth: 'web' }, serverOnly: 'private' },
42
+ i18n: { defaultLng: 'en', supportedLngs: ['en', 'pl'], serverOnly: 'private' },
43
+ [CONFIG_RECORD]: [
44
+ { id: 'plan:starter', recordType: 'plan', price: 10 },
45
+ { id: 'flow:signin', recordType: 'flow' },
46
+ ],
47
+ smtp: { host: 'smtp.example.test', pass: 'secret' },
48
+ queue: { queues: [{ name: 'private' }] },
49
+ dbs: [{ host: 'db.example.test', secret: 'secret' }],
50
+ secrets: { key: 'secret' },
51
+ } as unknown as CommonConfig)
52
+
53
+ expect(result).toEqual({
54
+ debug: { all: true, supervisor: true },
55
+ brand: { name: 'Example' },
56
+ security: { unsecure: false, auth: { login: { title: 'Sign in' } } },
57
+ services: {
58
+ api: { service: 'api', type: AppType.Backend, host: 'api.example.test', port: 443, base: 'api' },
59
+ },
60
+ plugins: [{ id: 'web', type: AppType.Frontend, value: 'safe' }],
61
+ oidc: {
62
+ clientCookie: { interaction: { name: 'oidc', ttl: 60 } },
63
+ providers: [{ clientId: 'public', service: 'iam' }],
64
+ },
65
+ flowConfig: { defaultFlow: 'signin', services: { auth: 'web' } },
66
+ i18n: { defaultLng: 'en', supportedLngs: ['en', 'pl'] },
67
+ [CONFIG_RECORD]: [{ id: 'plan:starter', recordType: 'plan', price: 10 }],
68
+ })
69
+ })
70
+
71
+ test('can remove nested secrets after allowing a collection', () => {
72
+ const result = selectApiConfig({
73
+ integrations: [
74
+ { name: 'public', token: 'secret', options: { endpoint: 'https://example.test', key: 'private' } },
75
+ ],
76
+ }, {
77
+ allow: { integrations: every(true) },
78
+ deny: { integrations: every({ token: true, options: { key: true } }) },
79
+ })
80
+
81
+ expect(result).toEqual({
82
+ integrations: [{ name: 'public', options: { endpoint: 'https://example.test' } }],
83
+ })
84
+ })
85
+ })
package/tsconfig.json CHANGED
@@ -6,5 +6,5 @@
6
6
  "rootDir": "./src/",
7
7
  "outDir": "./build/"
8
8
  },
9
- "exclude": ["./dist/**/*", "./build/**/*", "./*.ts"]
9
+ "exclude": ["./dist/**/*", "./build/**/*", "./tests/**/*", "./*.ts"]
10
10
  }