Repository navigation
Expand file tree
/
Copy pathdemo-database.mjs
More file actions
141 lines (136 loc) · 5.82 KB
/
Copy pathdemo-database.mjs
File metadata and controls
141 lines (136 loc) · 5.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
const invalidConfiguration = () => new Error('Local demo database configuration is invalid');
const aliases = new Map([
['host', 'host'],
['server', 'host'],
['port', 'port'],
['database', 'database'],
['username', 'username'],
['user name', 'username'],
['userid', 'username'],
['user id', 'username'],
['uid', 'username'],
['password', 'password'],
['pwd', 'password'],
['psw', 'password'],
]);
// The local demo accepts only these five fields, not the general Npgsql grammar.
// Quoted values preserve semicolons/whitespace and use doubled quote escaping.
function localConnectionFields(source) {
if (typeof source !== 'string' || /[\u0000-\u0008\u000a-\u001f\u007f]/.test(source))
throw invalidConfiguration();
const fields = new Map();
let index = 0;
while (index < source.length) {
while (/[; \t]/.test(source[index] ?? '') && index < source.length) index++;
if (index === source.length) break;
const start = index;
while (index < source.length && source[index] !== '=') {
if (!/[a-z \t]/i.test(source[index])) throw invalidConfiguration();
index++;
}
if (index === source.length) throw invalidConfiguration();
const key = aliases.get(source.slice(start, index).trim().toLowerCase());
if (!key || fields.has(key)) throw invalidConfiguration();
index++;
while (index < source.length && /[ \t]/.test(source[index])) index++;
let value = '';
const quote = source[index];
if (quote === '"' || quote === "'") {
index++;
let closed = false;
while (index < source.length) {
const character = source[index++];
if (character !== quote) value += character;
else if (source[index] === quote) {
value += quote;
index++;
} else {
closed = true;
break;
}
}
if (!closed) throw invalidConfiguration();
while (index < source.length && /[ \t]/.test(source[index])) index++;
if (index < source.length && source[index] !== ';') throw invalidConfiguration();
} else {
const start = index;
while (index < source.length && source[index] !== ';') {
if (source[index] === '"' || source[index] === "'") throw invalidConfiguration();
index++;
}
value = source.slice(start, index).trim();
}
if (!value) throw invalidConfiguration();
fields.set(key, value);
}
return fields;
}
/** Reject stale local credentials in the launcher without giving them to a child process. */
export function validateDemoDatabaseConfiguration(values) {
authorizationSourceEnvironment(values);
if (
values === null ||
values === undefined ||
typeof values.POSTGRES_DB !== 'string' ||
!values.POSTGRES_DB
)
throw invalidConfiguration();
for (const [suffix, role, passwordKey] of [
['Migration', 'learnstack_migration', 'LEARNSTACK_MIGRATION_PW'],
['Default', 'learnstack_app', 'LEARNSTACK_APP_PW'],
['PlatformAdmin', 'learnstack_platform', 'LEARNSTACK_PLATFORM_PW'],
['OutboxDispatcher', 'learnstack_outbox_admin', 'LEARNSTACK_OUTBOX_PW'],
]) {
const password = values[passwordKey];
const fields = localConnectionFields(values[`ConnectionStrings__${suffix}`]);
const port = fields.get('port') ?? '5432';
if (
typeof password !== 'string' ||
!password ||
fields.get('username') !== role ||
fields.get('password') !== password ||
fields.get('database') !== values.POSTGRES_DB ||
!['localhost', '127.0.0.1'].includes(fields.get('host')) ||
!/^\d+$/.test(port) ||
Number(port) !== 5432
)
throw invalidConfiguration();
}
}
/** An explicitly selected auxiliary pair is complete and nonempty, or absent. */
export function authorizationSourceEnvironment(values) {
const keys = ['ConnectionStrings__IdentityAuthorization', 'ConnectionStrings__AuthorizationEvidence'];
const supplied = keys.filter(key => values?.[key] !== undefined);
if (supplied.length !== 0 && (supplied.length !== keys.length
|| supplied.some(key => typeof values[key] !== 'string' || !values[key].trim() || /[\r\n\0]/.test(values[key]))))
throw invalidConfiguration();
return Object.fromEntries(supplied.map(key => [key, values[key]]));
}
/** Every Seeder mode receives only its own credential and process prerequisites. */
export function publicSeederEnvironment(applicationConnectionString, base = process.env) {
const environment = {};
for (const key of ['PATH', 'HOME', 'DOTNET_ROOT', 'TMPDIR', 'APPDATA',
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR',
'NUGET_PACKAGES', 'NUGET_HTTP_CACHE_PATH', 'COREPACK_HOME', 'PNPM_HOME', 'XDG_CACHE_HOME',
'NPM_CONFIG_CACHE', 'npm_config_cache', 'NPM_CONFIG_STORE_DIR', 'npm_config_store_dir'])
if (base[key] !== undefined) environment[key] = base[key];
if (applicationConnectionString !== undefined)
environment.ConnectionStrings__Default = applicationConnectionString;
return environment;
}
/** The owned local runtime receives its selected app-role lanes and installed authority. */
export function developmentSeederRuntimeEnvironment(values, base = process.env) {
const environment = publicSeederEnvironment(undefined, base);
for (const key of ['ConnectionStrings__Default', 'Identity__AuthorityId', 'Authentication__Tenant__Issuer']) {
if (typeof values?.[key] !== 'string' || !values[key].trim() || /[\r\n\0]/.test(values[key]))
throw invalidConfiguration();
environment[key] = values[key];
}
Object.assign(environment, authorizationSourceEnvironment(values));
// These callers own the documented direct-PostgreSQL Development stack.
// PgBouncer still requires its explicit pair and independent backend reserves.
environment.Deployment__Mode = 'Development';
return environment;
}