@owlmeans/api-config 0.1.18-rc.3 → 0.1.18-rc.30

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
@@ -1,46 +1,69 @@
1
1
  # @owlmeans/api-config
2
2
 
3
- Shared module for advertising safe config values from server to client via a REST endpoint.
3
+ Shared protocol for advertising safe config values from server to client via a REST endpoint.
4
4
 
5
5
  ## Overview
6
6
 
7
- - Exposes a `GET /assets/config.json` module that returns non-sensitive config fields
7
+ - Exposes the `advertise` protocol for `GET /assets/config.json`, returning package-allowlisted config fields
8
8
  - `ApiConfig` — the advertised config type (subset of `CommonConfig`)
9
- - `API_CONFIG` — module alias for the config endpoint
10
- - `notAdvertizedConfigKeys` / `allowedConfigRecords` — lists controlling what is/isn't exposed
9
+ - `API_CONFIG` — entrypoint alias for the config endpoint
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.30
16
16
  ```
17
17
 
18
18
  ## Usage
19
19
 
20
- Use with server and client counterparts — this package provides the shared types and module alias:
20
+ Use with server and client counterparts — this package provides the shared protocol, types, and
21
+ private adapter alias:
21
22
 
22
23
  ```typescript
23
- import { API_CONFIG } from '@owlmeans/api-config'
24
+ import { advertise } from '@owlmeans/api-config'
24
25
  import type { ApiConfig } from '@owlmeans/api-config'
26
+
27
+ // Bind this exact declaration in the server and browser packages.
28
+ context.entrypoint(advertise)
25
29
  ```
26
30
 
27
31
  ## API
28
32
 
29
33
  ### `ApiConfig`
30
34
 
31
- Subset of `CommonConfig` safe to expose to clients (no db credentials, secrets, etc.).
35
+ Partial public config assembled only from imported packages' allowlist plugins. Unregistered
36
+ server values — including databases, queues and secrets — are never returned.
32
37
 
33
38
  ### `API_CONFIG`
34
39
 
35
- Module alias `'api-config:advertise'` used to register/call the config endpoint.
40
+ Internal adapter alias `'api-config:advertise'`. Application code imports `advertise` instead of
41
+ looking the protocol up by this string.
42
+
43
+ ### `advertise`
44
+
45
+ The immutable, sticky protocol for `GET /assets/config.json`. It is the only shared declaration;
46
+ `@owlmeans/api-config-server` and `@owlmeans/api-config-client` each export local bindings for it.
47
+
48
+ ### `apiConfigPlugin`
36
49
 
37
- ### `modules`
50
+ Call this once at module scope in the package that owns a client-facing configuration field:
51
+
52
+ ```typescript
53
+ import { apiConfigPlugin, every } from '@owlmeans/api-config'
54
+
55
+ apiConfigPlugin({
56
+ allow: { integrations: every(true) },
57
+ deny: { integrations: every({ token: true }) },
58
+ })
59
+ ```
38
60
 
39
- Array of route definitions for the config advertisement endpoint.
61
+ The `deny` selector removes nested values after the allowlist selection. Prefer a precise nested
62
+ allowlist to `true`; `true` is appropriate only for data that is public at every depth.
40
63
 
41
64
  ## Related Packages
42
65
 
43
- - [`@owlmeans/api-config-server`](../api-config-server) — server-side module that serves the config
66
+ - [`@owlmeans/api-config-server`](../api-config-server) — server-side entrypoint that serves the config
44
67
  - [`@owlmeans/api-config-client`](../api-config-client) — client middleware that fetches and merges config
45
68
 
46
69
  <!-- owlmeans:agent-guidance:start -->
@@ -51,7 +74,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
51
74
  your project's skill store (`.agents/skills/`):
52
75
 
53
76
  ```sh
54
- npx @owlmeans/agent-skills
77
+ npx @owlmeans/agent-skills@^0.1.18-rc.31
55
78
  ```
56
79
 
57
80
  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.0",
5
- "generatedAt": "2026-08-16T22:20:50.515Z",
4
+ "version": "0.1.18-rc.30",
5
+ "generatedAt": "2026-09-21T21:54:22.978Z",
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,61 @@ user-invocable: false
8
8
  # @owlmeans/api-config
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/api-config": "^0.1.18-rc.0"` in `dependencies`
11
+ **Install:** `"@owlmeans/api-config": "^0.1.18-rc.30"` 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
+ | `advertise` | The single immutable declaration — route `/assets/config.json`, `sticky: true` so a router attaches it unconditionally |
22
+ | `API_CONFIG` | Internal adapter alias for `advertise`; do not use it for ordinary application lookup |
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 bound on both sides, so neither imports the other's package — they only share
32
+ this one. A server or browser package binds `advertise`; it does not import a flattened shared list:
33
+
34
+ ```typescript
35
+ import { advertise } from '@owlmeans/api-config'
36
+ import { bind } from '@owlmeans/server-entrypoint'
37
+
38
+ export const serverBindings = [bind(advertise, handler)]
39
+ ```
40
+
41
+ Most apps never do this directly: a backend built on `@owlmeans/server-app` already carries the
42
+ server side in its built-in `entrypoints`, and a client panel package already carries the client
43
+ side.
44
+
45
+ ## What may cross
46
+
47
+ The document is default-deny: a server config value is absent until the package that owns its
48
+ browser consumer registers a precise `allow` selector during import. Do not allow an ancestor with
49
+ `true` unless every descendant is public; name fields instead, and use `deny` when an otherwise
50
+ public collection carries nested credentials.
24
51
 
25
52
  ```typescript
26
- import { modules as apiConfigModules } from '@owlmeans/api-config'
27
- const appModules = [...apiConfigModules, ...myModules]
53
+ import { apiConfigPlugin, every } from '@owlmeans/api-config'
54
+
55
+ apiConfigPlugin({
56
+ allow: { oidc: { providers: every(true) } },
57
+ deny: { oidc: { providers: every({ secret: true, apiClientId: true }) } },
58
+ })
28
59
  ```
29
60
 
61
+ The base contract supplies public routes, branding, login settings, selected debug flags and
62
+ frontend plugins. `@owlmeans/oidc`, `@owlmeans/flow`, `@owlmeans/i18n` and `@owlmeans/payment`
63
+ register their own client settings during import. Databases, queues, SMTP, secrets and every
64
+ unregistered config extension never cross the unauthenticated endpoint.
65
+
30
66
  ## Depends On
31
67
 
32
68
  - `@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', 'layer', 'type',
4
- 'layerId', '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,OAAO,EAAE,MAAM;IACrD,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,gBAAgB;IAC5D,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"}
@@ -0,0 +1,3 @@
1
+ /** The public runtime-config endpoint protocol. */
2
+ export declare const advertise: import("@owlmeans/entrypoint").EntrypointProtocol<import("@owlmeans/entrypoint").OpenRequest, import("@owlmeans/entrypoint").OpenValue>;
3
+ //# sourceMappingURL=entrypoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entrypoints.d.ts","sourceRoot":"","sources":["../src/entrypoints.ts"],"names":[],"mappings":"AAIA,mDAAmD;AACnD,eAAO,MAAM,SAAS,yIAA2E,CAAA"}
@@ -0,0 +1,6 @@
1
+ import { openProtocol } from '@owlmeans/entrypoint';
2
+ import { route } from '@owlmeans/route';
3
+ import { API_CONFIG } from './consts.js';
4
+ /** The public runtime-config endpoint protocol. */
5
+ export const advertise = openProtocol(route(API_CONFIG, '/assets/config.json'), { sticky: true });
6
+ //# sourceMappingURL=entrypoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entrypoints.js","sourceRoot":"","sources":["../src/entrypoints.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,mDAAmD;AACnD,MAAM,CAAC,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,UAAU,EAAE,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,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
- export * from './modules.js';
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,cAAc,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
- export * from './modules.js';
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,cAAc,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' | 'layer' | 'type' | 'layerId'> {
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,OAAO,GAAG,MAAM,GAAG,SAAS,CACvE;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.3",
3
+ "version": "0.1.18-rc.30",
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.3",
25
- "@owlmeans/entrypoint": "^0.1.18-rc.3",
26
- "@owlmeans/route": "^0.1.18-rc.3"
25
+ "@owlmeans/config": "^0.1.18-rc.31",
26
+ "@owlmeans/context": "^0.1.18-rc.27",
27
+ "@owlmeans/entrypoint": "^0.1.18-rc.30",
28
+ "@owlmeans/route": "^0.1.18-rc.28"
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', 'layer', 'type',
6
- 'layerId', '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
+ })
@@ -0,0 +1,6 @@
1
+ import { openProtocol } from '@owlmeans/entrypoint'
2
+ import { route } from '@owlmeans/route'
3
+ import { API_CONFIG } from './consts.js'
4
+
5
+ /** The public runtime-config endpoint protocol. */
6
+ export const advertise = openProtocol(route(API_CONFIG, '/assets/config.json'), { sticky: true })
package/src/index.ts CHANGED
@@ -2,4 +2,6 @@
2
2
  export type * from './types.js'
3
3
 
4
4
  export * from './consts.js'
5
- export * from './modules.js'
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' | 'layer' | 'type' | 'layerId'
6
- > {
3
+ /** The public subset registered by packages through apiConfigPlugin(). */
4
+ export interface ApiConfig extends Partial<CommonConfig> {
7
5
  }
@@ -0,0 +1,79 @@
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
+ [CONFIG_RECORD]: [{ id: 'plan:starter', recordType: 'plan', price: 10 }],
62
+ })
63
+ })
64
+
65
+ test('can remove nested secrets after allowing a collection', () => {
66
+ const result = selectApiConfig({
67
+ integrations: [
68
+ { name: 'public', token: 'secret', options: { endpoint: 'https://example.test', key: 'private' } },
69
+ ],
70
+ }, {
71
+ allow: { integrations: every(true) },
72
+ deny: { integrations: every({ token: true, options: { key: true } }) },
73
+ })
74
+
75
+ expect(result).toEqual({
76
+ integrations: [{ name: 'public', options: { endpoint: 'https://example.test' } }],
77
+ })
78
+ })
79
+ })
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
  }
package/build/.gitkeep DELETED
File without changes
@@ -1,2 +0,0 @@
1
- export declare const modules: import("@owlmeans/entrypoint").CommonEntrypoint[];
2
- //# sourceMappingURL=modules.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../src/modules.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,OAAO,mDAEnB,CAAA"}
package/build/modules.js DELETED
@@ -1,7 +0,0 @@
1
- import { entrypoint } from '@owlmeans/entrypoint';
2
- import { route } from '@owlmeans/route';
3
- import { API_CONFIG } from './consts.js';
4
- export const modules = [
5
- entrypoint(route(API_CONFIG, '/assets/config.json'), { sticky: true }),
6
- ];
7
- //# sourceMappingURL=modules.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"modules.js","sourceRoot":"","sources":["../src/modules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAA;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,UAAU,CAAC,KAAK,CAAC,UAAU,EAAE,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;CACvE,CAAA"}
package/src/modules.ts DELETED
@@ -1,7 +0,0 @@
1
- import { entrypoint } from '@owlmeans/entrypoint'
2
- import { route } from '@owlmeans/route'
3
- import { API_CONFIG } from './consts.js'
4
-
5
- export const modules = [
6
- entrypoint(route(API_CONFIG, '/assets/config.json'), { sticky: true }),
7
- ]