@owlmeans/api-config 0.1.18-rc.3 → 0.1.18-rc.31
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 +36 -13
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/api-config/SKILL.md +44 -8
- package/build/consts.d.ts +0 -2
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +0 -8
- package/build/consts.js.map +1 -1
- package/build/defaults.d.ts +2 -0
- package/build/defaults.d.ts.map +1 -0
- package/build/defaults.js +33 -0
- package/build/defaults.js.map +1 -0
- package/build/entrypoints.d.ts +3 -0
- package/build/entrypoints.d.ts.map +1 -0
- package/build/entrypoints.js +6 -0
- package/build/entrypoints.js.map +1 -0
- package/build/index.d.ts +3 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -1
- package/build/index.js.map +1 -1
- package/build/plugins.d.ts +31 -0
- package/build/plugins.d.ts.map +1 -0
- package/build/plugins.js +121 -0
- package/build/plugins.js.map +1 -0
- package/build/types.d.ts +2 -1
- package/build/types.d.ts.map +1 -1
- package/package.json +6 -4
- package/src/consts.ts +0 -10
- package/src/defaults.ts +37 -0
- package/src/entrypoints.ts +6 -0
- package/src/index.ts +3 -1
- package/src/plugins.ts +161 -0
- package/src/types.ts +2 -4
- package/tests/plugins.spec.ts +79 -0
- package/tsconfig.json +1 -1
- package/build/.gitkeep +0 -0
- package/build/modules.d.ts +0 -2
- package/build/modules.d.ts.map +0 -1
- package/build/modules.js +0 -7
- package/build/modules.js.map +0 -1
- package/src/modules.ts +0 -7
package/README.md
CHANGED
|
@@ -1,46 +1,69 @@
|
|
|
1
1
|
# @owlmeans/api-config
|
|
2
2
|
|
|
3
|
-
Shared
|
|
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
|
|
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` —
|
|
10
|
-
- `
|
|
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.31
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
19
19
|
|
|
20
|
-
Use with server and client counterparts — this package provides the shared types and
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.32
|
|
55
78
|
```
|
|
56
79
|
|
|
57
80
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/api-config",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.31",
|
|
5
|
+
"generatedAt": "2026-09-22T12:36:21.873Z",
|
|
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 —
|
|
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.
|
|
11
|
+
**Install:** `"@owlmeans/api-config": "^0.1.18-rc.31"` 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
|
-
| `
|
|
18
|
-
|
|
|
19
|
-
|
|
|
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
|
-
|
|
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 {
|
|
27
|
-
|
|
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
package/build/consts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,UAAU,yBAAyB,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
|
package/build/consts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,UAAU,GAAG,sBAAsB,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 @@
|
|
|
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 @@
|
|
|
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
package/build/index.d.ts.map
CHANGED
|
@@ -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
package/build/index.js.map
CHANGED
|
@@ -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"}
|
package/build/plugins.js
ADDED
|
@@ -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
|
-
|
|
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
|
package/build/types.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
+
"version": "0.1.18-rc.31",
|
|
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.
|
|
25
|
-
"@owlmeans/
|
|
26
|
-
"@owlmeans/
|
|
25
|
+
"@owlmeans/config": "^0.1.18-rc.32",
|
|
26
|
+
"@owlmeans/context": "^0.1.18-rc.28",
|
|
27
|
+
"@owlmeans/entrypoint": "^0.1.18-rc.31",
|
|
28
|
+
"@owlmeans/route": "^0.1.18-rc.29"
|
|
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
|
-
]
|
package/src/defaults.ts
ADDED
|
@@ -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
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
|
-
|
|
4
|
-
|
|
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
package/build/.gitkeep
DELETED
|
File without changes
|
package/build/modules.d.ts
DELETED
package/build/modules.d.ts.map
DELETED
|
@@ -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
|
package/build/modules.js.map
DELETED
|
@@ -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