@voxgig/station 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,64 +0,0 @@
1
- "use strict";
2
- // @voxgig/station - one control surface for outbound integrations.
3
- //
4
- // THE BROWSER ENTRY (§2.2, §17 Phase 1), selected by the `browser`
5
- // condition in package.json `exports`. The same surface as `index.ts`
6
- // MINUS the file-loading half - `loadConfig`, `findConfigFile` and
7
- // `configScope` are node-only (`profile.ts`, the one module in this
8
- // package with top-level `node:` imports) and are simply not here, so
9
- // this entry's module graph pulls no `node:` builtin
10
- // (`test/browser.test.ts` walks the compiled graph and asserts it).
11
- //
12
- // `Station.open()` on this entry takes `config` as an OBJECT - the
13
- // pre-existing explicit-config path, which skips file loading entirely.
14
- // With no `config` at all it behaves exactly as node does when no
15
- // station.json exists: null config, repo-scoped. The ConfigFileIO seam
16
- // stays unregistered here; a host that genuinely has a config store can
17
- // register its own through `setConfigFileIO`.
18
- //
19
- // §2.2's honesty note stands: in the browser R1/R2 isolation does not
20
- // exist - an injected credential is readable by page code and DevTools
21
- // - so browser station is observability-only with app-held credentials
22
- // until a same-origin proxy endpoint exists.
23
- Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.validateConfig = exports.normalizeConfig = exports.configShape = exports.PROFILE_DEFAULTS = exports.MERGE_SENSITIVE = exports.BLOCK_DEFAULTS = exports.selectProfile = exports.resolveProfile = exports.refapi = exports.placeholderFor = exports.secretnameDefault = exports.normalizeDescriptor = exports.envtoken = exports.canonicalSerialize = exports.StationError = exports.featureBinding = exports.adapterFeature = exports.loadSync = exports.loadAsync = exports.factoryFromModule = exports.checkPackage = exports.camelify = exports.DEFAULT_EXPORT = exports.resetFactories = exports.provided = exports.factoryFor = exports.provide = exports.setConfigFileIO = exports.instanceRef = exports.Station = void 0;
25
- var Station_1 = require("./Station");
26
- Object.defineProperty(exports, "Station", { enumerable: true, get: function () { return Station_1.Station; } });
27
- Object.defineProperty(exports, "instanceRef", { enumerable: true, get: function () { return Station_1.instanceRef; } });
28
- Object.defineProperty(exports, "setConfigFileIO", { enumerable: true, get: function () { return Station_1.setConfigFileIO; } });
29
- var factory_1 = require("./factory");
30
- Object.defineProperty(exports, "provide", { enumerable: true, get: function () { return factory_1.provide; } });
31
- Object.defineProperty(exports, "factoryFor", { enumerable: true, get: function () { return factory_1.factoryFor; } });
32
- Object.defineProperty(exports, "provided", { enumerable: true, get: function () { return factory_1.provided; } });
33
- Object.defineProperty(exports, "resetFactories", { enumerable: true, get: function () { return factory_1.resetFactories; } });
34
- var loader_1 = require("./loader");
35
- Object.defineProperty(exports, "DEFAULT_EXPORT", { enumerable: true, get: function () { return loader_1.DEFAULT_EXPORT; } });
36
- Object.defineProperty(exports, "camelify", { enumerable: true, get: function () { return loader_1.camelify; } });
37
- Object.defineProperty(exports, "checkPackage", { enumerable: true, get: function () { return loader_1.checkPackage; } });
38
- Object.defineProperty(exports, "factoryFromModule", { enumerable: true, get: function () { return loader_1.factoryFromModule; } });
39
- Object.defineProperty(exports, "loadAsync", { enumerable: true, get: function () { return loader_1.loadAsync; } });
40
- Object.defineProperty(exports, "loadSync", { enumerable: true, get: function () { return loader_1.loadSync; } });
41
- var adapter_1 = require("./adapter");
42
- Object.defineProperty(exports, "adapterFeature", { enumerable: true, get: function () { return adapter_1.adapterFeature; } });
43
- Object.defineProperty(exports, "featureBinding", { enumerable: true, get: function () { return adapter_1.featureBinding; } });
44
- var error_1 = require("./error");
45
- Object.defineProperty(exports, "StationError", { enumerable: true, get: function () { return error_1.StationError; } });
46
- var descriptor_1 = require("./descriptor");
47
- Object.defineProperty(exports, "canonicalSerialize", { enumerable: true, get: function () { return descriptor_1.canonicalSerialize; } });
48
- Object.defineProperty(exports, "envtoken", { enumerable: true, get: function () { return descriptor_1.envtoken; } });
49
- Object.defineProperty(exports, "normalizeDescriptor", { enumerable: true, get: function () { return descriptor_1.normalizeDescriptor; } });
50
- Object.defineProperty(exports, "secretnameDefault", { enumerable: true, get: function () { return descriptor_1.secretnameDefault; } });
51
- var secrets_1 = require("./secrets");
52
- Object.defineProperty(exports, "placeholderFor", { enumerable: true, get: function () { return secrets_1.placeholderFor; } });
53
- var profilecore_1 = require("./profilecore");
54
- Object.defineProperty(exports, "refapi", { enumerable: true, get: function () { return profilecore_1.refapi; } });
55
- Object.defineProperty(exports, "resolveProfile", { enumerable: true, get: function () { return profilecore_1.resolveProfile; } });
56
- Object.defineProperty(exports, "selectProfile", { enumerable: true, get: function () { return profilecore_1.selectProfile; } });
57
- var shape_1 = require("./shape");
58
- Object.defineProperty(exports, "BLOCK_DEFAULTS", { enumerable: true, get: function () { return shape_1.BLOCK_DEFAULTS; } });
59
- Object.defineProperty(exports, "MERGE_SENSITIVE", { enumerable: true, get: function () { return shape_1.MERGE_SENSITIVE; } });
60
- Object.defineProperty(exports, "PROFILE_DEFAULTS", { enumerable: true, get: function () { return shape_1.PROFILE_DEFAULTS; } });
61
- Object.defineProperty(exports, "configShape", { enumerable: true, get: function () { return shape_1.configShape; } });
62
- Object.defineProperty(exports, "normalizeConfig", { enumerable: true, get: function () { return shape_1.normalizeConfig; } });
63
- Object.defineProperty(exports, "validateConfig", { enumerable: true, get: function () { return shape_1.validateConfig; } });
64
- //# sourceMappingURL=index.browser.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.browser.js","sourceRoot":"","sources":["../../src/index.browser.ts"],"names":[],"mappings":";AAAA,mEAAmE;AACnE,EAAE;AACF,mEAAmE;AACnE,sEAAsE;AACtE,mEAAmE;AACnE,oEAAoE;AACpE,sEAAsE;AACtE,qDAAqD;AACrD,oEAAoE;AACpE,EAAE;AACF,mEAAmE;AACnE,wEAAwE;AACxE,kEAAkE;AAClE,uEAAuE;AACvE,wEAAwE;AACxE,8CAA8C;AAC9C,EAAE;AACF,sEAAsE;AACtE,uEAAuE;AACvE,uEAAuE;AACvE,6CAA6C;;;AAE7C,qCAAiE;AAAxD,kGAAA,OAAO,OAAA;AAAE,sGAAA,WAAW,OAAA;AAAE,0GAAA,eAAe,OAAA;AAE9C,qCAAyE;AAAhE,kGAAA,OAAO,OAAA;AAAE,qGAAA,UAAU,OAAA;AAAE,mGAAA,QAAQ,OAAA;AAAE,yGAAA,cAAc,OAAA;AAEtD,mCAEiB;AADf,wGAAA,cAAc,OAAA;AAAE,kGAAA,QAAQ,OAAA;AAAE,sGAAA,YAAY,OAAA;AAAE,2GAAA,iBAAiB,OAAA;AAAE,mGAAA,SAAS,OAAA;AAAE,kGAAA,QAAQ,OAAA;AAEhF,qCAA0D;AAAjD,yGAAA,cAAc,OAAA;AAAE,yGAAA,cAAc,OAAA;AAEvC,iCAAsC;AAA7B,qGAAA,YAAY,OAAA;AACrB,2CAKqB;AAJnB,gHAAA,kBAAkB,OAAA;AAClB,sGAAA,QAAQ,OAAA;AACR,iHAAA,mBAAmB,OAAA;AACnB,+GAAA,iBAAiB,OAAA;AAEnB,qCAA0C;AAAjC,yGAAA,cAAc,OAAA;AACvB,6CAAqE;AAA5D,qGAAA,MAAM,OAAA;AAAE,6GAAA,cAAc,OAAA;AAAE,4GAAA,aAAa,OAAA;AAE9C,iCAGgB;AAFd,uGAAA,cAAc,OAAA;AAAE,wGAAA,eAAe,OAAA;AAAE,yGAAA,gBAAgB,OAAA;AACjD,oGAAA,WAAW,OAAA;AAAE,wGAAA,eAAe,OAAA;AAAE,uGAAA,cAAc,OAAA"}
@@ -1,10 +0,0 @@
1
- import { StationConfig, SdkBlock } from './types';
2
- export declare function selectProfile(optProfile?: string): string;
3
- export declare function refapi(ref: string): string;
4
- export type ResolvedProfile = {
5
- name: string;
6
- providers: any[];
7
- api: Record<string, SdkBlock>;
8
- sdk: Record<string, SdkBlock>;
9
- };
10
- export declare function resolveProfile(config: StationConfig | null, profileName: string): ResolvedProfile;
@@ -1,180 +0,0 @@
1
- "use strict";
2
- /* The pure half of profile handling: selection, the ref grammar, and
3
- * the §3.3 merge. Split from `profile.ts` so the BROWSER ENTRY can
4
- * reach it (§2.2, §17 Phase 1): everything here is data-in/data-out,
5
- * while the file half (findConfigFile/loadConfig/configScope) is
6
- * node-only and stays in `profile.ts`, imported by the node entry
7
- * alone. `profile.ts` re-exports these, so every existing import path
8
- * is unchanged. */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.selectProfile = selectProfile;
11
- exports.refapi = refapi;
12
- exports.resolveProfile = resolveProfile;
13
- const sekreto_1 = require("@voxgig/sekreto");
14
- const descriptor_1 = require("./descriptor");
15
- const error_1 = require("./error");
16
- const shape_1 = require("./shape");
17
- // Profile selection: VOXGIG_STATION_PROFILE, else the open() option,
18
- // else 'default' (design §3.5 - env vars rank above station.json but
19
- // below open() opts; profile NAME selection follows the same order with
20
- // open() opts winning).
21
- //
22
- // `process` is reached through a typeof guard rather than an import:
23
- // in the browser there is no process and no environment, so the answer
24
- // is simply the option or the default - which is the same degradation
25
- // "no env var set" already means.
26
- function selectProfile(optProfile) {
27
- if (null != optProfile && '' !== optProfile) {
28
- return optProfile;
29
- }
30
- const env = 'undefined' !== typeof process
31
- ? process.env.VOXGIG_STATION_PROFILE : undefined;
32
- if (null != env && '' !== env) {
33
- return env;
34
- }
35
- return 'default';
36
- }
37
- // The api half of a ref is the substring before the first `$`, and an
38
- // untagged ref IS an api slug (design §3.4) - which is why the whole
39
- // `plugin` -> `sdk` migration is a key rename and nothing else.
40
- //
41
- // LEXICAL, and that is the point: under the old free-form identity
42
- // which api an instance used was itself a merged value, so the merge
43
- // could not be a flat pass over both maps and a port that got the
44
- // phasing wrong silently picked another api's defaults (§3.3).
45
- function refapi(ref) {
46
- const at = String(ref).indexOf('$');
47
- return -1 === at ? String(ref) : String(ref).slice(0, at);
48
- }
49
- // Shallow merge, per key, left to right - each source over the one
50
- // before it. Declared rather than assumed: plugin's library default is
51
- // a deep map merge, and an allowlist that widens because two precedence
52
- // levels merged is the failure this rule exists to prevent
53
- // (station-and-plugin.md §2.5). The visible consequence: an overlay's
54
- // `policy` REPLACES the base's entirely rather than merging `hosts`
55
- // into it. That is also the safer reading for an allowlist.
56
- function shallow(...sources) {
57
- const out = {};
58
- for (const src of sources) {
59
- if (null == src || 'object' !== typeof src || Array.isArray(src)) {
60
- continue;
61
- }
62
- for (const k of Object.keys(src)) {
63
- out[k] = src[k];
64
- }
65
- }
66
- return out;
67
- }
68
- // Merge the base profile ('default') with the selected overlay.
69
- //
70
- // §3.3's total order, for the two block levels - lowest precedence
71
- // first:
72
- //
73
- // base.api[<api>] ⊕ base.sdk[<ref>] ⊕ overlay.api[<api>] ⊕ overlay.sdk[<ref>]
74
- //
75
- // PROFILE SPECIFICITY OUTRANKS BLOCK SPECIFICITY. A `prod` api-level
76
- // `base` beats a `default` instance-level `base`, because that is what
77
- // an environment overlay is for; within one profile the instance beats
78
- // the api default, because that is what an instance is for.
79
- //
80
- // THIS IS ONE FLAT LEFT-TO-RIGHT MERGE AND MUST NOT BE REORGANIZED INTO
81
- // "collapse each namespace, then put instance over api". Collapsing
82
- // first computes (base.api ⊕ overlay.api) and (base.sdk ⊕ overlay.sdk)
83
- // and then lets EVERY instance value beat EVERY api value - the exact
84
- // opposite. A production `api.stripe.policy` would then fail to
85
- // override a default profile's `sdk.stripe$test.policy`, silently
86
- // keeping the wider allowlist in production.
87
- //
88
- // `secrets.providers` replaces wholesale, never merges (§3.5, §5.2):
89
- // chain order decides which store wins, so a positional merge would be
90
- // actively dangerous.
91
- function resolveProfile(config, profileName) {
92
- const profiles = config?.profiles || {};
93
- const base = profiles['default'] || {};
94
- const overlay = 'default' === profileName
95
- ? {} : (profiles[profileName] || {});
96
- const providers = overlay.secrets?.providers ?? base.secrets?.providers ??
97
- [{ kind: 'env' }];
98
- const api = {};
99
- for (const slug of sortedkeys(base.api, overlay.api)) {
100
- api[slug] = shallow(base.api?.[slug], overlay.api?.[slug]);
101
- }
102
- // An api block declares no instance, so the ref set comes from the
103
- // two `sdk` maps alone.
104
- const sdk = {};
105
- for (const ref of sortedkeys(base.sdk, overlay.sdk)) {
106
- const a = refapi(ref);
107
- const merged = shallow(base.api?.[a], base.sdk?.[ref], overlay.api?.[a], overlay.sdk?.[ref]);
108
- // Defaults are applied ONCE, to the fully merged instance, and this
109
- // is the rule §4.2's normalization must not be confused with. Had
110
- // the overlay block carried a synthesized `active: true` into the
111
- // merge, a one-key environment override would silently re-enable an
112
- // integration the base declared `active: false` - live in
113
- // production despite being declared inactive. The `instance` corpus
114
- // section carries exactly this case.
115
- for (const k of Object.keys(shape_1.BLOCK_DEFAULTS)) {
116
- if (undefined === merged[k]) {
117
- merged[k] = shape_1.BLOCK_DEFAULTS[k]();
118
- }
119
- }
120
- sdk[ref] = merged;
121
- }
122
- checksecrets(sdk, profileName);
123
- return { name: profileName, providers, api, sdk };
124
- }
125
- // A configured secret name sekreto would reject is caught at profile
126
- // load, not first request (§14 station_secret_name) - and then the
127
- // derived names are checked for uniqueness, because envtoken is LOSSY.
128
- //
129
- // It collapses any run of non-alphanumerics to `_`, so `$` and `-` are
130
- // indistinguishable downstream: the ref `stripe$test` and an untagged
131
- // instance of an api slugged `stripe-test` both derive
132
- // `stripe_test.apikey`. That pairing is the realistic one rather than a
133
- // contrived one, because api slugs are hyphenated by construction - so
134
- // a fleet with both a `stripe-test` api and a `test` instance of
135
- // `stripe` collides by default. Since the resolution cache is keyed by
136
- // secret name (§5.3), the second instance would silently receive the
137
- // first's value.
138
- //
139
- // Two instances that EXPLICITLY name the same secret are not a
140
- // collision - that is the shared-key case the api-level `secret` exists
141
- // for, and saying so out loud is how you ask for it.
142
- function checksecrets(sdk, profileName) {
143
- const refs = Object.keys(sdk).sort();
144
- for (const ref of refs) {
145
- const name = sdk[ref].secret;
146
- if (null != name && !(0, sekreto_1.validname)(name)) {
147
- throw new error_1.StationError('station_secret_name', 'profile "' + profileName + '" sdk "' + ref +
148
- '": secret name rejected by sekreto: ' + JSON.stringify(name));
149
- }
150
- }
151
- const seen = new Map();
152
- for (const ref of refs) {
153
- const written = sdk[ref].secret;
154
- const derived = null == written || '' === written;
155
- const name = derived ? (0, descriptor_1.secretnameDefault)(ref) : written;
156
- const prior = seen.get(name);
157
- if (undefined !== prior && (derived || prior.derived)) {
158
- throw new error_1.StationError('station_secret_collision', 'profile "' + profileName + '": instances "' + prior.ref + '" and "' +
159
- ref + '" both resolve to secret name "' + name +
160
- '", so they would share one credential; name it explicitly on ' +
161
- 'each, or at the api level to share it deliberately (§5.1)');
162
- }
163
- if (undefined === prior) {
164
- seen.set(name, { ref, derived });
165
- }
166
- }
167
- }
168
- function sortedkeys(...maps) {
169
- const keys = new Set();
170
- for (const m of maps) {
171
- if (null == m || 'object' !== typeof m || Array.isArray(m)) {
172
- continue;
173
- }
174
- for (const k of Object.keys(m)) {
175
- keys.add(k);
176
- }
177
- }
178
- return Array.from(keys).sort();
179
- }
180
- //# sourceMappingURL=profilecore.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"profilecore.js","sourceRoot":"","sources":["../../src/profilecore.ts"],"names":[],"mappings":"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"}
@@ -1,56 +0,0 @@
1
- // @voxgig/station - one control surface for outbound integrations.
2
- //
3
- // THE BROWSER ENTRY (§2.2, §17 Phase 1), selected by the `browser`
4
- // condition in package.json `exports`. The same surface as `index.ts`
5
- // MINUS the file-loading half - `loadConfig`, `findConfigFile` and
6
- // `configScope` are node-only (`profile.ts`, the one module in this
7
- // package with top-level `node:` imports) and are simply not here, so
8
- // this entry's module graph pulls no `node:` builtin
9
- // (`test/browser.test.ts` walks the compiled graph and asserts it).
10
- //
11
- // `Station.open()` on this entry takes `config` as an OBJECT - the
12
- // pre-existing explicit-config path, which skips file loading entirely.
13
- // With no `config` at all it behaves exactly as node does when no
14
- // station.json exists: null config, repo-scoped. The ConfigFileIO seam
15
- // stays unregistered here; a host that genuinely has a config store can
16
- // register its own through `setConfigFileIO`.
17
- //
18
- // §2.2's honesty note stands: in the browser R1/R2 isolation does not
19
- // exist - an injected credential is readable by page code and DevTools
20
- // - so browser station is observability-only with app-held credentials
21
- // until a same-origin proxy endpoint exists.
22
-
23
- export { Station, instanceRef, setConfigFileIO } from './Station'
24
- export type { ConfigFileIO } from './Station'
25
- export { provide, factoryFor, provided, resetFactories } from './factory'
26
- export type { Factory, FactoryEntry } from './factory'
27
- export {
28
- DEFAULT_EXPORT, camelify, checkPackage, factoryFromModule, loadAsync, loadSync,
29
- } from './loader'
30
- export { adapterFeature, featureBinding } from './adapter'
31
- export type { FeatureBinding } from './adapter'
32
- export { StationError } from './error'
33
- export {
34
- canonicalSerialize,
35
- envtoken,
36
- normalizeDescriptor,
37
- secretnameDefault,
38
- } from './descriptor'
39
- export { placeholderFor } from './secrets'
40
- export { refapi, resolveProfile, selectProfile } from './profilecore'
41
- export type { ResolvedProfile } from './profilecore'
42
- export {
43
- BLOCK_DEFAULTS, MERGE_SENSITIVE, PROFILE_DEFAULTS,
44
- configShape, normalizeConfig, validateConfig,
45
- } from './shape'
46
- export type {
47
- Binding,
48
- Descriptor,
49
- PolicyBlock,
50
- Profile,
51
- ResolvedInstance,
52
- SdkBlock,
53
- StationConfig,
54
- StationEvent,
55
- StationOptions,
56
- } from './types'
@@ -1,201 +0,0 @@
1
- /* The pure half of profile handling: selection, the ref grammar, and
2
- * the §3.3 merge. Split from `profile.ts` so the BROWSER ENTRY can
3
- * reach it (§2.2, §17 Phase 1): everything here is data-in/data-out,
4
- * while the file half (findConfigFile/loadConfig/configScope) is
5
- * node-only and stays in `profile.ts`, imported by the node entry
6
- * alone. `profile.ts` re-exports these, so every existing import path
7
- * is unchanged. */
8
-
9
- import { validname } from '@voxgig/sekreto'
10
-
11
- import { secretnameDefault } from './descriptor'
12
- import { StationError } from './error'
13
- import { BLOCK_DEFAULTS } from './shape'
14
- import { StationConfig, SdkBlock } from './types'
15
-
16
- // Profile selection: VOXGIG_STATION_PROFILE, else the open() option,
17
- // else 'default' (design §3.5 - env vars rank above station.json but
18
- // below open() opts; profile NAME selection follows the same order with
19
- // open() opts winning).
20
- //
21
- // `process` is reached through a typeof guard rather than an import:
22
- // in the browser there is no process and no environment, so the answer
23
- // is simply the option or the default - which is the same degradation
24
- // "no env var set" already means.
25
- export function selectProfile(optProfile?: string): string {
26
- if (null != optProfile && '' !== optProfile) { return optProfile }
27
- const env = 'undefined' !== typeof process
28
- ? process.env.VOXGIG_STATION_PROFILE : undefined
29
- if (null != env && '' !== env) { return env }
30
- return 'default'
31
- }
32
-
33
- // The api half of a ref is the substring before the first `$`, and an
34
- // untagged ref IS an api slug (design §3.4) - which is why the whole
35
- // `plugin` -> `sdk` migration is a key rename and nothing else.
36
- //
37
- // LEXICAL, and that is the point: under the old free-form identity
38
- // which api an instance used was itself a merged value, so the merge
39
- // could not be a flat pass over both maps and a port that got the
40
- // phasing wrong silently picked another api's defaults (§3.3).
41
- export function refapi(ref: string): string {
42
- const at = String(ref).indexOf('$')
43
- return -1 === at ? String(ref) : String(ref).slice(0, at)
44
- }
45
-
46
- export type ResolvedProfile = {
47
- name: string
48
- providers: any[]
49
- // The api-level defaults in effect for this profile, base ⊕ overlay
50
- // per api slug. A REPORT, not an input to the instance merge below -
51
- // collapsing each namespace first and composing at the end is the
52
- // exact algorithm §3.3 forbids.
53
- api: Record<string, SdkBlock>
54
- // Instances, keyed by ref. An `api` block declares no instance of its
55
- // own (§3.1), so it never creates an entry here.
56
- sdk: Record<string, SdkBlock>
57
- }
58
-
59
- // Shallow merge, per key, left to right - each source over the one
60
- // before it. Declared rather than assumed: plugin's library default is
61
- // a deep map merge, and an allowlist that widens because two precedence
62
- // levels merged is the failure this rule exists to prevent
63
- // (station-and-plugin.md §2.5). The visible consequence: an overlay's
64
- // `policy` REPLACES the base's entirely rather than merging `hosts`
65
- // into it. That is also the safer reading for an allowlist.
66
- function shallow(...sources: any[]): any {
67
- const out: any = {}
68
- for (const src of sources) {
69
- if (null == src || 'object' !== typeof src || Array.isArray(src)) { continue }
70
- for (const k of Object.keys(src)) { out[k] = src[k] }
71
- }
72
- return out
73
- }
74
-
75
- // Merge the base profile ('default') with the selected overlay.
76
- //
77
- // §3.3's total order, for the two block levels - lowest precedence
78
- // first:
79
- //
80
- // base.api[<api>] ⊕ base.sdk[<ref>] ⊕ overlay.api[<api>] ⊕ overlay.sdk[<ref>]
81
- //
82
- // PROFILE SPECIFICITY OUTRANKS BLOCK SPECIFICITY. A `prod` api-level
83
- // `base` beats a `default` instance-level `base`, because that is what
84
- // an environment overlay is for; within one profile the instance beats
85
- // the api default, because that is what an instance is for.
86
- //
87
- // THIS IS ONE FLAT LEFT-TO-RIGHT MERGE AND MUST NOT BE REORGANIZED INTO
88
- // "collapse each namespace, then put instance over api". Collapsing
89
- // first computes (base.api ⊕ overlay.api) and (base.sdk ⊕ overlay.sdk)
90
- // and then lets EVERY instance value beat EVERY api value - the exact
91
- // opposite. A production `api.stripe.policy` would then fail to
92
- // override a default profile's `sdk.stripe$test.policy`, silently
93
- // keeping the wider allowlist in production.
94
- //
95
- // `secrets.providers` replaces wholesale, never merges (§3.5, §5.2):
96
- // chain order decides which store wins, so a positional merge would be
97
- // actively dangerous.
98
- export function resolveProfile(
99
- config: StationConfig | null, profileName: string
100
- ): ResolvedProfile {
101
- const profiles = config?.profiles || {}
102
- const base: any = profiles['default'] || {}
103
- const overlay: any = 'default' === profileName
104
- ? {} : (profiles[profileName] || {})
105
-
106
- const providers = overlay.secrets?.providers ?? base.secrets?.providers ??
107
- [{ kind: 'env' }]
108
-
109
- const api: Record<string, SdkBlock> = {}
110
- for (const slug of sortedkeys(base.api, overlay.api)) {
111
- api[slug] = shallow(base.api?.[slug], overlay.api?.[slug])
112
- }
113
-
114
- // An api block declares no instance, so the ref set comes from the
115
- // two `sdk` maps alone.
116
- const sdk: Record<string, SdkBlock> = {}
117
- for (const ref of sortedkeys(base.sdk, overlay.sdk)) {
118
- const a = refapi(ref)
119
- const merged = shallow(
120
- base.api?.[a],
121
- base.sdk?.[ref],
122
- overlay.api?.[a],
123
- overlay.sdk?.[ref],
124
- )
125
-
126
- // Defaults are applied ONCE, to the fully merged instance, and this
127
- // is the rule §4.2's normalization must not be confused with. Had
128
- // the overlay block carried a synthesized `active: true` into the
129
- // merge, a one-key environment override would silently re-enable an
130
- // integration the base declared `active: false` - live in
131
- // production despite being declared inactive. The `instance` corpus
132
- // section carries exactly this case.
133
- for (const k of Object.keys(BLOCK_DEFAULTS)) {
134
- if (undefined === merged[k]) { merged[k] = BLOCK_DEFAULTS[k]() }
135
- }
136
-
137
- sdk[ref] = merged
138
- }
139
-
140
- checksecrets(sdk, profileName)
141
-
142
- return { name: profileName, providers, api, sdk }
143
- }
144
-
145
- // A configured secret name sekreto would reject is caught at profile
146
- // load, not first request (§14 station_secret_name) - and then the
147
- // derived names are checked for uniqueness, because envtoken is LOSSY.
148
- //
149
- // It collapses any run of non-alphanumerics to `_`, so `$` and `-` are
150
- // indistinguishable downstream: the ref `stripe$test` and an untagged
151
- // instance of an api slugged `stripe-test` both derive
152
- // `stripe_test.apikey`. That pairing is the realistic one rather than a
153
- // contrived one, because api slugs are hyphenated by construction - so
154
- // a fleet with both a `stripe-test` api and a `test` instance of
155
- // `stripe` collides by default. Since the resolution cache is keyed by
156
- // secret name (§5.3), the second instance would silently receive the
157
- // first's value.
158
- //
159
- // Two instances that EXPLICITLY name the same secret are not a
160
- // collision - that is the shared-key case the api-level `secret` exists
161
- // for, and saying so out loud is how you ask for it.
162
- function checksecrets(
163
- sdk: Record<string, SdkBlock>, profileName: string
164
- ): void {
165
- const refs = Object.keys(sdk).sort()
166
-
167
- for (const ref of refs) {
168
- const name = sdk[ref].secret
169
- if (null != name && !validname(name)) {
170
- throw new StationError('station_secret_name',
171
- 'profile "' + profileName + '" sdk "' + ref +
172
- '": secret name rejected by sekreto: ' + JSON.stringify(name))
173
- }
174
- }
175
-
176
- const seen = new Map<string, { ref: string, derived: boolean }>()
177
- for (const ref of refs) {
178
- const written = sdk[ref].secret
179
- const derived = null == written || '' === written
180
- const name = derived ? secretnameDefault(ref) : (written as string)
181
-
182
- const prior = seen.get(name)
183
- if (undefined !== prior && (derived || prior.derived)) {
184
- throw new StationError('station_secret_collision',
185
- 'profile "' + profileName + '": instances "' + prior.ref + '" and "' +
186
- ref + '" both resolve to secret name "' + name +
187
- '", so they would share one credential; name it explicitly on ' +
188
- 'each, or at the api level to share it deliberately (§5.1)')
189
- }
190
- if (undefined === prior) { seen.set(name, { ref, derived }) }
191
- }
192
- }
193
-
194
- function sortedkeys(...maps: any[]): string[] {
195
- const keys = new Set<string>()
196
- for (const m of maps) {
197
- if (null == m || 'object' !== typeof m || Array.isArray(m)) { continue }
198
- for (const k of Object.keys(m)) { keys.add(k) }
199
- }
200
- return Array.from(keys).sort()
201
- }