@o3co/create-auth-provider 0.15.0 → 0.17.0-rc.1
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.ja.md +139 -48
- package/README.md +138 -47
- package/dist/index.d.mts +33 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +118 -59
- package/dist/internal/template-filter.d.mts +7 -10
- package/dist/internal/template-filter.d.mts.map +1 -1
- package/dist/internal/template-filter.mjs +7 -10
- package/package.json +6 -4
- package/templates/standalone/.dockerignore +15 -2
- package/templates/standalone/.env.example +69 -24
- package/templates/standalone/Dockerfile +7 -3
- package/templates/standalone/README.ja.md +677 -184
- package/templates/standalone/README.md +699 -238
- package/templates/standalone/config/application.conf +203 -325
- package/templates/standalone/config/clients.yaml +2 -0
- package/templates/standalone/config/clients.yaml.example +4 -0
- package/templates/standalone/config/development.conf +13 -4
- package/templates/standalone/config/mailpit.conf +8 -0
- package/templates/standalone/config/production.conf +5 -6
- package/templates/standalone/config/reference.conf +399 -0
- package/templates/standalone/docker-compose.mailpit.yml +36 -0
- package/templates/standalone/docker-compose.production.yml +58 -30
- package/templates/standalone/docker-compose.yml +13 -3
- package/templates/standalone/gitignore +4 -2
- package/templates/standalone/package.json +9 -6
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +65 -32
- package/templates/standalone/src/__tests__/adapters.test.mts +358 -0
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +1178 -0
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +256 -0
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +1119 -0
- package/templates/standalone/src/__tests__/audit-sink.test.mts +78 -76
- package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
- package/templates/standalone/src/__tests__/core-federations.test.mts +226 -0
- package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +9 -14
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +672 -271
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +480 -78
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +102 -66
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +135 -0
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +49 -18
- package/templates/standalone/src/__tests__/google-session.fixture.mts +315 -0
- package/templates/standalone/src/__tests__/host-routes.test.mts +134 -0
- package/templates/standalone/src/__tests__/library-references.fixture.mts +183 -0
- package/templates/standalone/src/__tests__/listen.test.mts +121 -0
- package/templates/standalone/src/__tests__/logger-projection.test.mts +77 -0
- package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +328 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +17 -7
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +780 -0
- package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +52 -46
- package/templates/standalone/src/__tests__/own-modules.test.mts +1457 -0
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +183 -0
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +135 -0
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +112 -81
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
- package/templates/standalone/src/__tests__/replica-safety.test.mts +166 -145
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +146 -0
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +275 -95
- package/templates/standalone/src/__tests__/sections.test.mts +480 -0
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +67 -55
- package/templates/standalone/src/__tests__/session-requirements.test.mts +299 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +319 -115
- package/templates/standalone/src/__tests__/smoke.test.mts +179 -233
- package/templates/standalone/src/__tests__/store-credential.test.mts +317 -0
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +108 -95
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +438 -0
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +390 -0
- package/templates/standalone/src/adapters.mts +149 -0
- package/templates/standalone/src/app.mts +71 -115
- package/templates/standalone/src/buildModules.mts +229 -251
- package/templates/standalone/src/configPath.mts +422 -29
- package/templates/standalone/src/listen.mts +60 -0
- package/templates/standalone/src/logger.mts +31 -57
- package/templates/standalone/src/metrics.mts +24 -47
- package/templates/standalone/src/mfaSwitch.mts +262 -0
- package/templates/standalone/src/modules.mts +480 -464
- package/templates/standalone/src/rootRenames.mts +79 -0
- package/templates/standalone/src/routes.mts +89 -0
- package/templates/standalone/src/sections.mts +366 -0
- package/templates/standalone/src/shutdown.mts +54 -126
- package/templates/standalone/tests/index.test.js +2 -2
- package/templates/standalone/tests/package-lock.json +1615 -2223
- package/templates/standalone/tests/package.json +2 -2
- package/templates/standalone/tests/vitest.config.mjs +26 -0
- package/templates/standalone/vitest.config.mts +8 -7
- package/templates/standalone/vitest.supertest-loopback.mts +97 -69
- package/templates/versions.json +18 -15
- package/templates/standalone/src/__tests__/terminal-error.test.mts +0 -96
- package/templates/standalone/src/terminalError.mts +0 -60
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The template's posture on session admission. What it expects is its
|
|
19
|
+
* configuration's `core.sessionRequirements.expected`
|
|
20
|
+
* — `[]` in the shipped `config/application.conf` — with `mfa` added when the
|
|
21
|
+
* template's MFA switch, `mfaMode` (`MFA_MODE`), is not `off`
|
|
22
|
+
* (`expectedSessionRequirements`); under that switch the template installs
|
|
23
|
+
* the MFA module, whose requirement registers `mfa`. And what it hands boot
|
|
24
|
+
* of the MFA module's section, `mfa`: nothing unless a loaded module owns it,
|
|
25
|
+
* its mode written from the switch when the template installs MFA, and a
|
|
26
|
+
* mode the configuration writes that the switch does not say refused.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
30
|
+
import { tmpdir } from "node:os";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
import { BootError, type Module } from "@o3co/auth-provider-core";
|
|
33
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
34
|
+
import { buildModules } from "../buildModules.mjs";
|
|
35
|
+
import {
|
|
36
|
+
expectedSessionRequirements,
|
|
37
|
+
readOwnLayers,
|
|
38
|
+
readSwitches,
|
|
39
|
+
resolveForBoot,
|
|
40
|
+
resolveLayers,
|
|
41
|
+
type Switches,
|
|
42
|
+
} from "../configPath.mjs";
|
|
43
|
+
import {
|
|
44
|
+
type Composition,
|
|
45
|
+
compose,
|
|
46
|
+
ownFiles,
|
|
47
|
+
SINGLE_ENV,
|
|
48
|
+
} from "./all-modules-composition.fixture.mjs";
|
|
49
|
+
|
|
50
|
+
let current: Composition | undefined;
|
|
51
|
+
|
|
52
|
+
afterEach(async () => {
|
|
53
|
+
await current?.handle.dispose();
|
|
54
|
+
current = undefined;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** The boot's refusal, or `undefined` when it booted (kept for disposal). */
|
|
58
|
+
const refusal = (composing: Promise<Composition>): Promise<unknown> =>
|
|
59
|
+
composing.then(
|
|
60
|
+
(composition) => {
|
|
61
|
+
current = composition;
|
|
62
|
+
return undefined;
|
|
63
|
+
},
|
|
64
|
+
(caught: unknown) => caught,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/** What MFA on needs in production beside the switch: a key, and a mail relay. */
|
|
68
|
+
const MFA_ENV: Readonly<Record<string, string>> = {
|
|
69
|
+
MFA_ENCRYPTION_KEY: Buffer.alloc(32, 9).toString("base64"),
|
|
70
|
+
STANDARD_SMTP_MAIL_SENDER_HOST: "smtp.auth.test",
|
|
71
|
+
STANDARD_SMTP_MAIL_SENDER_FROM: "auth@auth.test",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
describe("what the template expects of session admission", () => {
|
|
75
|
+
it("is [] in the shipped configuration under MFA_MODE=off, and the composition boots with it", async () => {
|
|
76
|
+
const own = readOwnLayers(ownFiles(), { env: SINGLE_ENV });
|
|
77
|
+
expect(resolveLayers(own, []).core).toMatchObject({ sessionRequirements: { expected: [] } });
|
|
78
|
+
expect(readSwitches(own).mfaMode).toBe("off");
|
|
79
|
+
current = await compose();
|
|
80
|
+
expect(current.config).not.toHaveProperty("mfa");
|
|
81
|
+
expect(current.config.core?.sessionRequirements).toEqual({ expected: [] });
|
|
82
|
+
expect([...(current.handle.components.sessionRequirementResolver?.entries() ?? [])]).toEqual(
|
|
83
|
+
[],
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it.each(["optional", "required"] as const)(
|
|
88
|
+
"adds mfa under MFA_MODE=%s, which the MFA module the template installs registers",
|
|
89
|
+
async (mode) => {
|
|
90
|
+
current = await compose({
|
|
91
|
+
env: { ...SINGLE_ENV, ...MFA_ENV, MFA_MODE: mode },
|
|
92
|
+
environment: "test",
|
|
93
|
+
});
|
|
94
|
+
expect(current.config.core?.sessionRequirements).toEqual({
|
|
95
|
+
expected: ["mfa"],
|
|
96
|
+
secondFactorAuthority: "mfa",
|
|
97
|
+
});
|
|
98
|
+
expect(
|
|
99
|
+
[...(current.handle.components.sessionRequirementResolver?.entries() ?? [])].map(
|
|
100
|
+
([name]) => name,
|
|
101
|
+
),
|
|
102
|
+
).toEqual(["mfa"]);
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
it("keeps an operator's own list and adds mfa beside it, overwriting nothing", async () => {
|
|
107
|
+
const err = await refusal(
|
|
108
|
+
compose({
|
|
109
|
+
env: { ...SINGLE_ENV, ...MFA_ENV, MFA_MODE: "required" },
|
|
110
|
+
environment: "test",
|
|
111
|
+
operatorHocon: 'core.sessionRequirements.expected = ["risk"]\n',
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
expect(err).toBeInstanceOf(BootError);
|
|
115
|
+
expect((err as BootError).reason).toBe("session-requirement-missing");
|
|
116
|
+
expect((err as BootError).details).toMatchObject({
|
|
117
|
+
missing: ["risk"],
|
|
118
|
+
declared: ["risk", "mfa"],
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("refuses the boot when the configuration expects mfa under the switch off: nothing registers it", async () => {
|
|
123
|
+
const err = await refusal(
|
|
124
|
+
compose({ operatorHocon: 'core.sessionRequirements.expected = ["mfa"]\n' }),
|
|
125
|
+
);
|
|
126
|
+
expect(err).toBeInstanceOf(BootError);
|
|
127
|
+
expect((err as BootError).reason).toBe("session-requirement-missing");
|
|
128
|
+
expect((err as BootError).details).toMatchObject({
|
|
129
|
+
configKey: "core.sessionRequirements.expected",
|
|
130
|
+
missing: ["mfa"],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("what the template hands boot of the mfa section", () => {
|
|
136
|
+
const ownUnder = (env: Readonly<Record<string, string>>, hocon?: string) =>
|
|
137
|
+
readOwnLayers(hocon === undefined ? ownFiles() : [operatorLayer(hocon), ...ownFiles()], {
|
|
138
|
+
env,
|
|
139
|
+
});
|
|
140
|
+
/** A module whose section is `mfa`: resolveForBoot reads its name and section alone. */
|
|
141
|
+
const reader = { name: "mfa", section: {} } as unknown as Module;
|
|
142
|
+
const mfaOf = (resolved: unknown) => (resolved as { mfa?: unknown }).mfa;
|
|
143
|
+
|
|
144
|
+
it("hands none under the switch off unless a loaded module owns the section, whatever the configuration writes there", () => {
|
|
145
|
+
const own = ownUnder(SINGLE_ENV, 'mfa.page.url = "/my-mfa"\nmfa.mdoe = "required"\n');
|
|
146
|
+
const switches = readSwitches(own);
|
|
147
|
+
expect(resolveForBoot(own, [], switches)).not.toHaveProperty("mfa");
|
|
148
|
+
// A module the deployment adds that owns it: handed as written.
|
|
149
|
+
expect(mfaOf(resolveForBoot(own, [reader], switches))).toEqual({
|
|
150
|
+
page: { url: "/my-mfa" },
|
|
151
|
+
mdoe: "required",
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("hands it, owned by the MFA module the switch installs, with the mode written from the switch over the MFA package's default", () => {
|
|
156
|
+
for (const mode of ["optional", "required"] as const) {
|
|
157
|
+
// The switch written in a layer, MFA_MODE unset: the package's reference says off.
|
|
158
|
+
const { MFA_MODE: _unset, ...env } = SINGLE_ENV;
|
|
159
|
+
const own = ownUnder(env, `mfaMode = "${mode}"\n`);
|
|
160
|
+
const switches = readSwitches(own);
|
|
161
|
+
const modules = buildModules(switches, { environment: "test" });
|
|
162
|
+
expect(mfaOf(resolveForBoot(own, modules, switches)), mode).toMatchObject({ mode });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it.each([
|
|
167
|
+
["the switch off", {}, 'mfa.mode = "required"\n'],
|
|
168
|
+
["the switch off, the mode optional", {}, 'mfa.mode = "optional"\n'],
|
|
169
|
+
["MFA_MODE=optional", { MFA_MODE: "optional" }, 'mfa.mode = "required"\n'],
|
|
170
|
+
["MFA_MODE=required", { MFA_MODE: "required" }, 'mfa.mode = "optional"\n'],
|
|
171
|
+
])(
|
|
172
|
+
"refuses, under %s, an mfa.mode the configuration writes that the switch does not say, naming both keys and quoting nothing",
|
|
173
|
+
(_label, env, hocon) => {
|
|
174
|
+
const own = ownUnder({ ...SINGLE_ENV, ...env }, hocon);
|
|
175
|
+
const switches = readSwitches(own);
|
|
176
|
+
let err: unknown;
|
|
177
|
+
try {
|
|
178
|
+
resolveForBoot(own, [reader], switches);
|
|
179
|
+
} catch (caught) {
|
|
180
|
+
err = caught;
|
|
181
|
+
}
|
|
182
|
+
expect(err).toBeInstanceOf(RangeError);
|
|
183
|
+
const message = (err as RangeError).message;
|
|
184
|
+
expect(message).toContain("mfa.mode");
|
|
185
|
+
expect(message).toContain("mfaMode");
|
|
186
|
+
expect(message).not.toMatch(/"(?:off|optional|required)"/);
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
it.each([
|
|
191
|
+
["the switch off", {}],
|
|
192
|
+
["MFA_MODE=required", { MFA_MODE: "required" }],
|
|
193
|
+
])(
|
|
194
|
+
"refuses, under %s, an mfa written as a value, naming mfa itself and quoting nothing",
|
|
195
|
+
(_label, env) => {
|
|
196
|
+
const own = ownUnder({ ...SINGLE_ENV, ...env }, 'mfa = "required"\n');
|
|
197
|
+
let err: unknown;
|
|
198
|
+
try {
|
|
199
|
+
resolveForBoot(own, [reader], readSwitches(own));
|
|
200
|
+
} catch (caught) {
|
|
201
|
+
err = caught;
|
|
202
|
+
}
|
|
203
|
+
expect(err).toBeInstanceOf(RangeError);
|
|
204
|
+
const message = (err as RangeError).message;
|
|
205
|
+
expect(message).toMatch(/^mfa is written as a value/);
|
|
206
|
+
expect(message).not.toContain("remove mfa.mode");
|
|
207
|
+
expect(message).toContain("mfaMode");
|
|
208
|
+
expect(message).not.toMatch(/"(?:off|optional|required)"/);
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
it("accepts an mfa.mode the configuration writes that the switch says", () => {
|
|
213
|
+
for (const [env, hocon] of [
|
|
214
|
+
[{}, 'mfa.mode = "off"\n'],
|
|
215
|
+
[{ MFA_MODE: "required" }, 'mfa.mode = "required"\n'],
|
|
216
|
+
] as const) {
|
|
217
|
+
const own = ownUnder({ ...SINGLE_ENV, ...env }, hocon);
|
|
218
|
+
expect(() => resolveForBoot(own, [reader], readSwitches(own)), hocon).not.toThrow();
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("is refused through the composition as through phase two: before boot, under the switch off", async () => {
|
|
223
|
+
const err = await refusal(compose({ operatorHocon: 'mfa.mode = "required"\n' }));
|
|
224
|
+
expect(err).toBeInstanceOf(RangeError);
|
|
225
|
+
expect(err).not.toBeInstanceOf(BootError);
|
|
226
|
+
expect((err as RangeError).message).toContain("mfaMode");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("expectedSessionRequirements", () => {
|
|
231
|
+
const of = (mfa: string, expected?: readonly string[], authority?: string) =>
|
|
232
|
+
expectedSessionRequirements({
|
|
233
|
+
mfaMode: mfa,
|
|
234
|
+
...(expected === undefined && authority === undefined
|
|
235
|
+
? {}
|
|
236
|
+
: {
|
|
237
|
+
core: {
|
|
238
|
+
sessionRequirements: {
|
|
239
|
+
...(expected === undefined ? {} : { expected }),
|
|
240
|
+
...(authority === undefined ? {} : { secondFactorAuthority: authority }),
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
} as unknown as Switches);
|
|
245
|
+
const AUTHORITY = { secondFactorAuthority: "mfa" } as const;
|
|
246
|
+
|
|
247
|
+
it("is the configuration's list, with mfa added once and named the second-factor authority when the switch is not off", () => {
|
|
248
|
+
expect(of("off", ["risk"])).toEqual({ expected: ["risk"] });
|
|
249
|
+
expect(of("optional", ["risk"])).toEqual({ expected: ["risk", "mfa"], ...AUTHORITY });
|
|
250
|
+
expect(of("required", ["mfa"])).toEqual({ expected: ["mfa"], ...AUTHORITY });
|
|
251
|
+
expect(of("required")).toEqual({ expected: ["mfa"], ...AUTHORITY });
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("keeps the configuration's list as written, repeats included, and adds mfa only when it is absent", () => {
|
|
255
|
+
expect(of("required", ["risk", "risk"])).toEqual({
|
|
256
|
+
expected: ["risk", "risk", "mfa"],
|
|
257
|
+
...AUTHORITY,
|
|
258
|
+
});
|
|
259
|
+
expect(of("required", ["mfa", "risk", "mfa"])).toEqual({
|
|
260
|
+
expected: ["mfa", "risk", "mfa"],
|
|
261
|
+
...AUTHORITY,
|
|
262
|
+
});
|
|
263
|
+
expect(of("off", ["risk", "risk"])).toEqual({ expected: ["risk", "risk"] });
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("accepts a written second-factor authority the switch says, and keeps one written under the switch off", () => {
|
|
267
|
+
expect(of("required", ["mfa"], "mfa")).toEqual({ expected: ["mfa"], ...AUTHORITY });
|
|
268
|
+
expect(of("off", ["risk"], "risk")).toEqual({
|
|
269
|
+
expected: ["risk"],
|
|
270
|
+
secondFactorAuthority: "risk",
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("refuses, with the switch on, a written second-factor authority other than mfa, naming the key and the switch and quoting nothing", () => {
|
|
275
|
+
let err: unknown;
|
|
276
|
+
try {
|
|
277
|
+
of("required", ["mfa", "sentinel-requirement"], "sentinel-requirement");
|
|
278
|
+
} catch (caught) {
|
|
279
|
+
err = caught;
|
|
280
|
+
}
|
|
281
|
+
expect(err).toBeInstanceOf(RangeError);
|
|
282
|
+
const message = (err as RangeError).message;
|
|
283
|
+
expect(message).toContain("core.sessionRequirements.secondFactorAuthority");
|
|
284
|
+
expect(message).toContain("mfaMode");
|
|
285
|
+
expect(message).toContain("MFA_MODE");
|
|
286
|
+
expect(message).not.toContain("sentinel-requirement");
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("declares nothing when the configuration writes no list and the switch is off, so boot's own rule for an unwritten key applies", () => {
|
|
290
|
+
expect(of("off")).toBeUndefined();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
/** `text` in a file of its own, a layer above the composition's files. */
|
|
295
|
+
function operatorLayer(text: string): string {
|
|
296
|
+
const file = join(mkdtempSync(join(tmpdir(), "session-requirements-")), "operator.conf");
|
|
297
|
+
writeFileSync(file, text);
|
|
298
|
+
return file;
|
|
299
|
+
}
|