proteum 2.1.0-4 → 2.1.0-5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,7 +36,7 @@ Proteum combines:
36
36
  ```text
37
37
  my-app/
38
38
  identity.yaml
39
- env.yaml
39
+ .env # optional file for required local env vars
40
40
  package.json
41
41
  client/
42
42
  pages/
@@ -63,7 +63,7 @@ my-app/
63
63
  Important files:
64
64
 
65
65
  - `identity.yaml`: app identity, naming, locale, and SEO-facing metadata defaults
66
- - `env.yaml`: environment contract loaded by the app
66
+ - `process.env` / optional `.env`: `PORT`, `ENV_*`, `URL`, and `TRACE_*` environment variables loaded by the app
67
67
  - `server/config/*.ts`: plain typed config exports consumed by the explicit app bootstrap
68
68
  - `server/index.ts`: default-exported `Application` subclass that instantiates root services and router plugins
69
69
  - `client/pages/**`: SSR page entrypoints registered through `Router.page(...)`
@@ -71,6 +71,25 @@ Important files:
71
71
  - `server/services/**`: business logic that extends `Service`
72
72
  - `.proteum/**`: framework-owned generated contracts and manifests
73
73
 
74
+ Required Proteum env vars:
75
+
76
+ - `ENV_NAME`: `local` or `server`
77
+ - `ENV_PROFILE`: `dev`, `testing`, or `prod`
78
+ - `PORT`: default router port
79
+ - `URL`: canonical absolute base URL for `Router.url(..., true)`
80
+
81
+ Proteum does not provide defaults for required env vars. They must be defined explicitly in `process.env` or `.env`.
82
+
83
+ Use `proteum explain env` to see the required env vars, their allowed values, and whether each one is currently provided.
84
+
85
+ Optional trace env vars:
86
+
87
+ - `TRACE_ENABLE`
88
+ - `TRACE_REQUESTS_LIMIT`
89
+ - `TRACE_EVENTS_LIMIT`
90
+ - `TRACE_CAPTURE`
91
+ - `TRACE_PERSIST_ON_ERROR`
92
+
74
93
  ## Example: Server Bootstrap
75
94
 
76
95
  Proteum app services are declared explicitly through typed config exports plus a concrete `Application` subclass.
@@ -87,7 +106,7 @@ type RouterBaseConfig = Omit<ServiceConfig<typeof Router>, 'plugins'>;
87
106
  export const usersConfig = Services.config(Users, {});
88
107
 
89
108
  export const routerBaseConfig = {
90
- domains: AppContainer.Environment.router.domains,
109
+ currentDomain: AppContainer.Environment.router.currentDomain,
91
110
  http: {
92
111
  domain: 'example.com',
93
112
  port: AppContainer.Environment.router.port,
@@ -265,7 +284,7 @@ proteum trace latest
265
284
 
266
285
  Proteum includes a dev-only in-memory request trace buffer for routing, controller, context, SSR, and render debugging.
267
286
 
268
- When diagnosing or testing against an app, first read the default port from `env.yaml` and check whether a server is already running there. If it is, inspect the existing traces before reproducing the issue so you can collect past errors and their context.
287
+ When diagnosing or testing against an app, first read the default port from `PORT` or `./.proteum/manifest.json` and check whether a server is already running there. If it is, inspect the existing traces before reproducing the issue so you can collect past errors and their context.
269
288
 
270
289
  - `proteum trace requests`: list the most recent request summaries
271
290
  - `proteum trace latest`: show the latest captured request
@@ -277,19 +296,18 @@ When diagnosing or testing against an app, first read the default port from `env
277
296
  Default behavior:
278
297
 
279
298
  - tracing is enabled only in `profile: dev`
280
- - traces live in memory and are bounded by `trace.requestsLimit` and `trace.eventsLimit`
299
+ - traces live in memory and are bounded by `TRACE_REQUESTS_LIMIT` and `TRACE_EVENTS_LIMIT`
281
300
  - payloads are summarized, long strings are truncated, and sensitive fields such as cookies, passwords, and tokens are redacted
282
- - `persistOnError` can export crashing requests under `var/traces/`
301
+ - `TRACE_PERSIST_ON_ERROR` can export crashing requests under `var/traces/`
283
302
 
284
- `env.yaml` example:
303
+ Trace env example:
285
304
 
286
- ```yaml
287
- trace:
288
- enable: true
289
- requestsLimit: 200
290
- eventsLimit: 800
291
- capture: resolve
292
- persistOnError: true
305
+ ```bash
306
+ export TRACE_ENABLE=true
307
+ export TRACE_REQUESTS_LIMIT=200
308
+ export TRACE_EVENTS_LIMIT=800
309
+ export TRACE_CAPTURE=resolve
310
+ export TRACE_PERSIST_ON_ERROR=true
293
311
  ```
294
312
 
295
313
  Capture modes:
@@ -314,7 +332,7 @@ Proteum is built so an agent can answer these questions quickly and reliably:
314
332
  Proteum answers those questions with explicit artifacts:
315
333
 
316
334
  - `identity.yaml` for app identity
317
- - `env.yaml` for the environment surface
335
+ - `PORT`, `ENV_*`, `URL`, and `TRACE_*` env vars for the environment surface
318
336
  - `server/index.ts` for the explicit root service graph
319
337
  - `.proteum/manifest.json` for machine-readable app structure
320
338
  - `proteum explain --json` for structured framework introspection
@@ -323,7 +341,7 @@ Proteum answers those questions with explicit artifacts:
323
341
  If you are an LLM or automation agent, start here:
324
342
 
325
343
  1. Read `identity.yaml`.
326
- 2. Read `env.yaml`.
344
+ 2. Read `PORT`, the relevant `ENV_*`, `URL`, and `TRACE_*` env vars, or run `proteum explain env`.
327
345
  3. Inspect `server/index.ts` and `server/config/*.ts` for the explicit app bootstrap.
328
346
  4. Read `.proteum/manifest.json` or run `proteum explain --json`.
329
347
  5. Inspect `server/controllers/**` for request entrypoints.
@@ -12,7 +12,7 @@ When you enter a Proteum app, inspect it in this order:
12
12
  2. Inspect `./server/index.ts` and `./server/config/*.ts`.
13
13
  3. Inspect the touched `./server/controllers/**/*.ts`, `./server/services/**`, `./server/routes/**`, and `./client/pages/**` files.
14
14
  4. Run `npx proteum doctor` if routing or generation looks suspicious.
15
- 5. If you need to diagnose or test against a running app, check the default port in `./env.yaml` first.
15
+ 5. If you need to diagnose or test against a running app, check the default port in `PORT` or `./.proteum/manifest.json` first.
16
16
  6. If a server is already running on that port, use `npx proteum trace` to inspect past requests, errors, and their context before reproducing the issue or adding temporary logs.
17
17
 
18
18
  ## Non-Negotiable Rules
@@ -38,7 +38,7 @@ Proteum reads these source files directly:
38
38
 
39
39
  - `package.json`
40
40
  - `identity.yaml`
41
- - `env.yaml`
41
+ - `process.env` via the `PORT`, `ENV_*`, `URL`, and `TRACE_*` env contract
42
42
  - `server/config/*.ts`
43
43
  - `server/index.ts`
44
44
  - `server/services/**/service.json`
@@ -239,7 +239,7 @@ Relevant aliases:
239
239
 
240
240
  1. Run `npx proteum explain --json`.
241
241
  2. Run `npx proteum doctor`.
242
- 3. Read the default port from `./env.yaml` and check whether a server is already running there.
242
+ 3. Read the default port from `PORT` or `./.proteum/manifest.json` and check whether a server is already running there.
243
243
  4. If a server is already running on that default port, inspect existing traces first:
244
244
  - `npx proteum trace requests --port <envPort>`
245
245
  - `npx proteum trace latest --port <envPort>`
@@ -282,7 +282,7 @@ Verify at the correct layer:
282
282
 
283
283
  When you need to diagnose or test against an app that may already be running:
284
284
 
285
- - read the default port from `env.yaml`
285
+ - read the default port from `PORT` or `./.proteum/manifest.json`
286
286
  - check whether a server is already running on that port
287
287
  - if it is, inspect `proteum trace requests`, `proteum trace latest`, and `proteum trace show <requestId>` before reproducing the issue
288
288
 
@@ -27,7 +27,7 @@ For request-time issues in dev, inspect traces before adding temporary logs:
27
27
 
28
28
  If you need to diagnose or test against a running app:
29
29
 
30
- - read the default port from `./env.yaml`
30
+ - read the default port from `PORT` or `./.proteum/manifest.json`
31
31
  - check whether a server is already running on that port
32
32
  - if it is, inspect existing traces first to collect past errors and their context before reproducing the issue
33
33
 
@@ -83,13 +83,13 @@ When a feature depends on a curated list, keep one canonical catalog or registry
83
83
  1. evaluate or quantify the probability
84
84
  2. explain why
85
85
  3. suggest how to fix it
86
- - When the issue is request-time behavior in dev, first check whether a server is already running on the default port from `env.yaml`. If it is, prefer `npx proteum trace` to inspect past errors and their context before reproducing the issue or adding logs.
86
+ - When the issue is request-time behavior in dev, first check whether a server is already running on the default port from `PORT` or `./.proteum/manifest.json`. If it is, prefer `npx proteum trace` to inspect past errors and their context before reproducing the issue or adding logs.
87
87
  - When you have finished your work, summarize in one top-level short sentence the changes you made since the beginning of the conversation. Output as `Commit message`.
88
88
 
89
89
  ## High-Impact Files
90
90
 
91
91
  - `tsconfig*.json`
92
- - `env*.yaml`
92
+ - `PORT`, `ENV_*`, `URL`, and `TRACE_*` env setup
93
93
  - Prisma-generated files
94
94
  - symbolic links
95
95
 
package/cli/app/config.ts CHANGED
@@ -2,20 +2,12 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
- /*
6
- NOTE: This is a copy of core/sever/app/config
7
- We can't import core deps here because it will cause the following error:
8
- "Can't use import when not a module"
9
- It will be possible to import core files when the CLI will be compiled as one output file with tsc
10
- And for that, we need to fix the TS errors for the CLI
11
- */
12
-
13
5
  // Npm
14
6
  import fs from 'fs-extra';
15
7
  import yaml from 'yaml';
16
8
 
17
9
  // Types
18
- import type { TEnvConfig } from '../../server/app/container/config';
10
+ import { parseProteumEnvConfig, type TProteumLoadedEnvConfig } from '../../common/env/proteumEnv';
19
11
  import { logVerbose } from '../runtime/verbose';
20
12
 
21
13
  /*----------------------------------
@@ -34,18 +26,13 @@ export default class ConfigParser {
34
26
  return yaml.parse(rawConfig);
35
27
  }
36
28
 
37
- public env(): TEnvConfig {
38
- // We assume that when we run 5htp dev, we're in local
39
- // Otherwise, we're in production environment (docker)
40
- logVerbose('[app] Using environment:', process.env.NODE_ENV);
41
- const envFileName = this.appDir + '/env.yaml';
42
- const envFile = this.loadYaml(envFileName);
29
+ public env(): TProteumLoadedEnvConfig {
30
+ logVerbose('[app] Loading Proteum env vars from process.env');
43
31
  return {
44
- ...envFile,
45
- router:
46
- this.routerPortOverride === undefined
47
- ? envFile.router
48
- : { ...envFile.router, port: this.routerPortOverride },
32
+ ...parseProteumEnvConfig({
33
+ appDir: this.appDir,
34
+ routerPortOverride: this.routerPortOverride,
35
+ }),
49
36
  version: 'CLI',
50
37
  };
51
38
  }
@@ -45,8 +45,7 @@ export async function run() {
45
45
  { spaces: 4 },
46
46
  );
47
47
 
48
- // Copy config file
49
- fs.copyFileSync(app.paths.root + (simulate ? '/env.yaml' : '/env.server.yaml'), temp + '/env.yaml');
48
+ // Deployment now relies on exported ENV_*, URL, TRACE_*, and PORT variables instead of copied env config files.
50
49
 
51
50
  // Compile & Run Docker
52
51
  await cli.shell(`docker compose up --build`);
@@ -115,10 +115,11 @@ const printSection = (title: string, lines: string[]) => {
115
115
  const renderSummary = (manifest: TProteumManifest) => {
116
116
  const errorsCount = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length;
117
117
  const warningsCount = manifest.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length;
118
+ const providedRequiredEnvVariables = manifest.env.requiredVariables.filter((variable) => variable.provided).length;
118
119
  const lines = [
119
120
  `Proteum manifest: ${formatFilepath(manifest, path.join(manifest.app.root, '.proteum', 'manifest.json'))}`,
120
121
  `App: ${manifest.app.identity.name} (${manifest.app.identity.identifier})`,
121
- `Env keys: ${manifest.env.loadedTopLevelKeys.join(', ') || 'none'}`,
122
+ `Env vars: ${providedRequiredEnvVariables}/${manifest.env.requiredVariables.length} required provided`,
122
123
  `Services: ${manifest.services.app.length} app, ${manifest.services.routerPlugins.length} router plugins`,
123
124
  `Controllers: ${manifest.controllers.length}`,
124
125
  `Routes: ${manifest.routes.client.length} client, ${manifest.routes.server.length} server`,
@@ -163,9 +164,16 @@ const renderHuman = (manifest: TProteumManifest, sectionNames: TExplainSectionNa
163
164
  if (sectionName === 'env') {
164
165
  sections.push(
165
166
  printSection('Env', [
166
- `- source=${formatFilepath(manifest, manifest.env.sourceFilepath)}`,
167
- `- loadedTopLevelKeys=${manifest.env.loadedTopLevelKeys.join(', ') || 'none'}`,
168
- `- requiredTopLevelKeys=${manifest.env.requiredTopLevelKeys.join(', ')}`,
167
+ `- source=${manifest.env.source}`,
168
+ `- loadedVariableKeys=${manifest.env.loadedVariableKeys.join(', ') || 'none'}`,
169
+ ...manifest.env.requiredVariables.map(
170
+ (variable) =>
171
+ `- ${variable.key} possibleValues=${variable.possibleValues.join(' | ')} provided=${variable.provided ? 'yes' : 'no'}`,
172
+ ),
173
+ `- resolved.name=${manifest.env.resolved.name}`,
174
+ `- resolved.profile=${manifest.env.resolved.profile}`,
175
+ `- resolved.routerPort=${manifest.env.resolved.routerPort}`,
176
+ `- resolved.routerCurrentDomain=${manifest.env.resolved.routerCurrentDomain}`,
169
177
  ]),
170
178
  );
171
179
  continue;
@@ -1,7 +1,6 @@
1
1
  import fs from 'fs-extra';
2
2
  import got from 'got';
3
3
  import path from 'path';
4
- import yaml from 'yaml';
5
4
  import { UsageError } from 'clipanion';
6
5
 
7
6
  import cli from '..';
@@ -12,7 +11,7 @@ import type {
12
11
  TRequestTraceListItem,
13
12
  TRequestTraceListResponse,
14
13
  TRequestTraceResponse,
15
- } from '@common/dev/requestTrace';
14
+ } from '../../common/dev/requestTrace';
16
15
 
17
16
  type TTraceAction = 'latest' | 'show' | 'requests' | 'arm' | 'export';
18
17
 
@@ -31,22 +30,33 @@ const getAction = () => {
31
30
 
32
31
  const normalizeBaseUrl = (value: string) => value.replace(/\/+$/, '');
33
32
 
33
+ const getRouterPortFromManifest = () => {
34
+ const manifestFilepath = path.join(cli.args.workdir as string, '.proteum', 'manifest.json');
35
+ if (!fs.existsSync(manifestFilepath)) return undefined;
36
+
37
+ const manifest = fs.readJsonSync(manifestFilepath, { throws: false }) as
38
+ | { env?: { resolved?: { routerPort?: number } } }
39
+ | undefined;
40
+ const port = manifest?.env?.resolved?.routerPort;
41
+
42
+ if (typeof port !== 'number' || port <= 0) return undefined;
43
+
44
+ return String(port);
45
+ };
46
+
34
47
  const getRouterPort = () => {
35
48
  const overridePort = typeof cli.args.port === 'string' && cli.args.port ? cli.args.port : '';
36
49
  if (overridePort) return overridePort;
37
50
 
38
- const envFilepath = path.join(cli.args.workdir as string, 'env.yaml');
39
- if (!fs.existsSync(envFilepath)) {
40
- throw new UsageError(`Could not find env.yaml in ${cli.args.workdir as string}. Pass --port or --url explicitly.`);
41
- }
51
+ const envPort = process.env.PORT?.trim();
52
+ if (envPort) return envPort;
42
53
 
43
- const envFile = yaml.parse(fs.readFileSync(envFilepath, 'utf8')) as { router?: { port?: number } };
44
- const port = envFile.router?.port;
45
- if (!port) {
46
- throw new UsageError(`Could not determine the router port from ${envFilepath}. Pass --port or --url explicitly.`);
47
- }
54
+ const manifestPort = getRouterPortFromManifest();
55
+ if (manifestPort) return manifestPort;
48
56
 
49
- return String(port);
57
+ throw new UsageError(
58
+ `Could not determine the router port from PORT or .proteum/manifest.json in ${cli.args.workdir as string}. Pass --port or --url explicitly.`,
59
+ );
50
60
  };
51
61
 
52
62
  const getRouterBaseUrls = () => {
@@ -1,9 +1,10 @@
1
1
  import path from 'path';
2
- import fs from 'fs-extra';
3
- import yaml from 'yaml';
4
2
 
5
3
  import app from '../../app';
6
4
  import cli from '../..';
5
+ import {
6
+ inspectProteumEnv,
7
+ } from '../../../common/env/proteumEnv';
7
8
  import { reservedRouteSetupKeys, routeSetupOptionKeys } from '../../../common/router/pageSetup';
8
9
  import {
9
10
  TProteumManifest,
@@ -15,20 +16,6 @@ import {
15
16
  import { writeProteumManifest } from '../common/proteumManifest';
16
17
  import { normalizeAbsolutePath, normalizePath } from './shared';
17
18
 
18
- const envRequiredTopLevelKeys = ['name', 'profile', 'router', 'console'];
19
-
20
- const getEnvTopLevelKeys = () => {
21
- const envFilepath = path.join(app.paths.root, 'env.yaml');
22
-
23
- if (!fs.existsSync(envFilepath)) return [];
24
-
25
- const rawEnv = yaml.parse(fs.readFileSync(envFilepath, 'utf8'));
26
-
27
- if (!rawEnv || typeof rawEnv !== 'object' || Array.isArray(rawEnv)) return [];
28
-
29
- return Object.keys(rawEnv).sort((a, b) => a.localeCompare(b));
30
- };
31
-
32
19
  const collectManifestDiagnostics = ({
33
20
  controllers,
34
21
  routes,
@@ -228,6 +215,8 @@ export const writeCurrentProteumManifest = ({
228
215
  routes: TProteumManifest['routes'];
229
216
  layouts: TProteumManifestLayout[];
230
217
  }) => {
218
+ const envInspection = inspectProteumEnv(app.paths.root);
219
+
231
220
  const manifest: TProteumManifest = {
232
221
  version: 1,
233
222
  app: {
@@ -252,9 +241,19 @@ export const writeCurrentProteumManifest = ({
252
241
  reservedRouteSetupKeys: [...reservedRouteSetupKeys],
253
242
  },
254
243
  env: {
255
- sourceFilepath: normalizeAbsolutePath(path.join(app.paths.root, 'env.yaml')),
256
- loadedTopLevelKeys: getEnvTopLevelKeys(),
257
- requiredTopLevelKeys: [...envRequiredTopLevelKeys],
244
+ source: 'process.env',
245
+ loadedVariableKeys: envInspection.loadedVariableKeys,
246
+ requiredVariables: envInspection.requiredVariables.map((variable) => ({
247
+ key: variable.key,
248
+ possibleValues: [...variable.possibleValues],
249
+ provided: variable.provided,
250
+ })),
251
+ resolved: {
252
+ name: app.env.name,
253
+ profile: app.env.profile,
254
+ routerPort: app.env.router.port,
255
+ routerCurrentDomain: app.env.router.currentDomain,
256
+ },
258
257
  },
259
258
  services,
260
259
  controllers,
@@ -80,7 +80,7 @@ export default function createCommonConfig(
80
80
  APP_NAME: JSON.stringify(app.identity.web.title),
81
81
  APP_OUTPUT_DIR: JSON.stringify(path.basename(app.outputPath(outputTarget))),
82
82
  PROTEUM_DEV_EVENT_PORT: JSON.stringify(dev ? (app.devEventPort ?? null) : null),
83
- PROTEUM_ROUTER_PORT_OVERRIDE: JSON.stringify(app.routerPortOverride ?? null),
83
+ PROTEUM_PORT_OVERRIDE: JSON.stringify(app.routerPortOverride ?? null),
84
84
  }),
85
85
 
86
86
  ...(dev ? [] : []),
@@ -100,9 +100,19 @@ export type TProteumManifest = {
100
100
  reservedRouteSetupKeys: string[];
101
101
  };
102
102
  env: {
103
- sourceFilepath: string;
104
- loadedTopLevelKeys: string[];
105
- requiredTopLevelKeys: string[];
103
+ source: string;
104
+ loadedVariableKeys: string[];
105
+ requiredVariables: {
106
+ key: string;
107
+ possibleValues: string[];
108
+ provided: boolean;
109
+ }[];
110
+ resolved: {
111
+ name: string;
112
+ profile: string;
113
+ routerPort: number;
114
+ routerCurrentDomain: string;
115
+ };
106
116
  };
107
117
  services: {
108
118
  app: TProteumManifestService[];
@@ -202,7 +202,7 @@ export const proteumCommands: Record<TProteumCommandName, TProteumCommandDoc> =
202
202
  notes: [
203
203
  'This command talks to the running app over the dev-only `__proteum/trace` HTTP endpoints.',
204
204
  'Traces are stored in a bounded in-memory buffer with payload summarization and sensitive-field redaction.',
205
- 'Use `--port` when the app is not running on the router port declared in `env.yaml`, or `--url` when the host itself is non-standard.',
205
+ 'Use `--port` when the app is not running on the router port declared in `PORT`, or `--url` when the host itself is non-standard.',
206
206
  ],
207
207
  status: 'experimental',
208
208
  },
@@ -38,16 +38,9 @@ export default function App({ context }: { context: ClientContext }) {
38
38
  <DialogManager />
39
39
 
40
40
  {!layout ? (
41
- <>
42
- {/* TODO: move to app, because here, we're not aware that the router service has been defined */}
43
- <RouterComponent service={context.Router} />
44
- </>
41
+ <RouterComponent service={context.Router} />
45
42
  ) : (
46
- <>
47
- {' '}
48
- {/* Same as router/components/Page.tsx */}
49
- <layout.Component {...layoutProps} />
50
- </>
43
+ <layout.Component {...layoutProps} />
51
44
  )}
52
45
  </ReactClientContext.Provider>
53
46
  );
@@ -21,7 +21,6 @@ import BaseRouter, {
21
21
  TErrorRoute,
22
22
  TRouteOptions,
23
23
  TRouteModule,
24
- TDomainsList,
25
24
  matchRoute,
26
25
  buildUrl,
27
26
  } from '@common/router';
@@ -136,7 +135,7 @@ export default class ClientRouter<
136
135
  // Context data
137
136
  public ssrRoutes = browserWindow.routes || [];
138
137
  public ssrContext = browserWindow.ssr;
139
- public domains: TDomainsList = browserWindow.ssr?.domains || ({ current: window.location.origin } as TDomainsList);
138
+ public currentDomain = browserWindow.ssr?.currentDomain || window.location.origin;
140
139
  public context!: TRouterContext<this, this['app']>;
141
140
 
142
141
  public setLoading!: React.Dispatch<React.SetStateAction<boolean>>;
@@ -153,7 +152,7 @@ export default class ClientRouter<
153
152
  }
154
153
 
155
154
  public url = (path: string, params: {} = {}, absolute: boolean = true) =>
156
- buildUrl(path, params, this.domains, absolute);
155
+ buildUrl(path, params, this.currentDomain, absolute);
157
156
 
158
157
  public go(url: string | number, data: {} = {}, opt: { newTab?: boolean } = {}) {
159
158
  // Error code
@@ -1,6 +1,7 @@
1
1
  export const traceCaptureModes = ['summary', 'resolve', 'deep'] as const;
2
2
 
3
3
  export type TTraceCaptureMode = (typeof traceCaptureModes)[number];
4
+ type TTracePrimitive = string | number | boolean;
4
5
 
5
6
  export const traceEventTypes = [
6
7
  'request.start',
@@ -27,7 +28,7 @@ export const traceEventTypes = [
27
28
  export type TTraceEventType = (typeof traceEventTypes)[number];
28
29
 
29
30
  export type TTraceSummaryValue =
30
- | PrimitiveValue
31
+ | TTracePrimitive
31
32
  | null
32
33
  | { kind: 'undefined' }
33
34
  | { kind: 'redacted'; reason: string }
@@ -0,0 +1,284 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import dotenv from 'dotenv';
4
+
5
+ export type TProteumEnvName = 'local' | 'server';
6
+ export type TProteumEnvProfile = 'dev' | 'testing' | 'prod';
7
+ export type TProteumTraceCapture = 'summary' | 'resolve' | 'deep';
8
+ export type TProteumRequiredEnvVariable = {
9
+ key: TProteumRequiredEnvVariableKey;
10
+ possibleValues: string[];
11
+ provided: boolean;
12
+ };
13
+ export type TProteumEnvInspection = {
14
+ loadedVariableKeys: string[];
15
+ requiredVariables: TProteumRequiredEnvVariable[];
16
+ };
17
+
18
+ export type TProteumEnvConfig = {
19
+ name: TProteumEnvName;
20
+ profile: TProteumEnvProfile;
21
+ router: {
22
+ port: number;
23
+ currentDomain: string;
24
+ };
25
+ trace: {
26
+ enable: boolean;
27
+ requestsLimit: number;
28
+ eventsLimit: number;
29
+ capture: TProteumTraceCapture;
30
+ persistOnError: boolean;
31
+ };
32
+ };
33
+
34
+ export type TProteumLoadedEnvConfig = TProteumEnvConfig & { version: string };
35
+
36
+ const dotenvFileNames = ['.env'];
37
+ const requiredProteumEnvVariableKeys = ['ENV_NAME', 'ENV_PROFILE', 'PORT', 'URL'] as const;
38
+ const optionalProteumEnvVariablePrefixes = ['TRACE_'] as const;
39
+
40
+ export type TProteumRequiredEnvVariableKey = (typeof requiredProteumEnvVariableKeys)[number];
41
+
42
+ const requiredProteumEnvVariablePossibleValues: Record<TProteumRequiredEnvVariableKey, string[]> = {
43
+ ENV_NAME: ['local', 'server'],
44
+ ENV_PROFILE: ['dev', 'testing', 'prod'],
45
+ PORT: ['integer between 1 and 65535'],
46
+ URL: ['absolute URL'],
47
+ };
48
+
49
+ const envDefinitionHint = (appDir: string) => `Define it in process.env or ${appDir}/.env.`;
50
+ const isProvidedEnvValue = (value: string | undefined) => typeof value === 'string' && value.trim() !== '';
51
+
52
+ const formatRequiredEnvVariableStatus = (variable: TProteumRequiredEnvVariable) =>
53
+ `- ${variable.key} possibleValues=${variable.possibleValues.join(' | ')} provided=${variable.provided ? 'yes' : 'no'}`;
54
+
55
+ const createProteumEnvError = ({
56
+ appDir,
57
+ message,
58
+ }: {
59
+ appDir: string;
60
+ message: string;
61
+ }) => {
62
+ const inspection = inspectProteumEnv(appDir);
63
+
64
+ return new Error(
65
+ [message, envDefinitionHint(appDir), '', 'Required env variables:', ...inspection.requiredVariables.map(formatRequiredEnvVariableStatus)].join(
66
+ '\n',
67
+ ),
68
+ );
69
+ };
70
+
71
+ const parseBooleanEnvValue = ({
72
+ key,
73
+ value,
74
+ appDir,
75
+ }: {
76
+ key: string;
77
+ value: string | undefined;
78
+ appDir: string;
79
+ }) => {
80
+ if (value === undefined || value === '') return undefined;
81
+
82
+ const normalized = value.trim().toLowerCase();
83
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
84
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
85
+
86
+ throw createProteumEnvError({
87
+ appDir,
88
+ message: `Invalid boolean value for ${key}: "${value}". Expected one of: 1, 0, true, false, yes, no, on, off.`,
89
+ });
90
+ };
91
+
92
+ const parseIntegerEnvValue = ({
93
+ key,
94
+ value,
95
+ appDir,
96
+ min = 1,
97
+ }: {
98
+ key: string;
99
+ value: string;
100
+ appDir: string;
101
+ min?: number;
102
+ }) => {
103
+ const parsed = Number.parseInt(value, 10);
104
+ if (Number.isNaN(parsed) || parsed < min) {
105
+ throw createProteumEnvError({
106
+ appDir,
107
+ message: `Invalid integer value for ${key}: "${value}". Expected an integer greater than or equal to ${min}.`,
108
+ });
109
+ }
110
+
111
+ return parsed;
112
+ };
113
+
114
+ const getRequiredEnvValue = ({ key, appDir }: { key: TProteumRequiredEnvVariableKey; appDir: string }) => {
115
+ const value = process.env[key]?.trim();
116
+ if (value) return value;
117
+
118
+ throw createProteumEnvError({
119
+ appDir,
120
+ message: `Missing required Proteum env variable "${key}".`,
121
+ });
122
+ };
123
+
124
+ const parseEnvName = (value: string, appDir: string): TProteumEnvName => {
125
+ if (value === 'local' || value === 'server') return value;
126
+ throw createProteumEnvError({
127
+ appDir,
128
+ message: `Invalid ENV_NAME "${value}". Expected "local" or "server".`,
129
+ });
130
+ };
131
+
132
+ const parseEnvProfile = (value: string, appDir: string): TProteumEnvProfile => {
133
+ if (value === 'dev' || value === 'testing' || value === 'prod') return value;
134
+ throw createProteumEnvError({
135
+ appDir,
136
+ message: `Invalid ENV_PROFILE "${value}". Expected "dev", "testing", or "prod".`,
137
+ });
138
+ };
139
+
140
+ const parseTraceCapture = ({
141
+ value,
142
+ appDir,
143
+ }: {
144
+ value: string | undefined;
145
+ appDir: string;
146
+ }): TProteumTraceCapture | undefined => {
147
+ if (value === undefined || value === '') return undefined;
148
+ if (value === 'summary' || value === 'resolve' || value === 'deep') return value;
149
+
150
+ throw createProteumEnvError({
151
+ appDir,
152
+ message: `Invalid TRACE_CAPTURE "${value}". Expected "summary", "resolve", or "deep".`,
153
+ });
154
+ };
155
+
156
+ const parseAbsoluteUrl = ({
157
+ key,
158
+ value,
159
+ appDir,
160
+ }: {
161
+ key: string;
162
+ value: string;
163
+ appDir: string;
164
+ }) => {
165
+ let url: URL;
166
+
167
+ try {
168
+ url = new URL(value);
169
+ } catch {
170
+ throw createProteumEnvError({
171
+ appDir,
172
+ message: `Invalid absolute URL for ${key}: "${value}". Expected an absolute http:// or https:// URL.`,
173
+ });
174
+ }
175
+
176
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
177
+ throw createProteumEnvError({
178
+ appDir,
179
+ message: `Invalid absolute URL for ${key}: "${value}". Expected an absolute http:// or https:// URL.`,
180
+ });
181
+ }
182
+
183
+ return value;
184
+ };
185
+
186
+ export const loadOptionalProteumDotenv = (appDir: string) => {
187
+ for (const filename of dotenvFileNames) {
188
+ const filepath = path.join(appDir, filename);
189
+ if (!fs.existsSync(filepath)) continue;
190
+ dotenv.config({ path: filepath, quiet: true });
191
+ }
192
+ };
193
+
194
+ export const getLoadedProteumEnvVariableKeys = () =>
195
+ Object.keys(process.env)
196
+ .filter(
197
+ (key) =>
198
+ requiredProteumEnvVariableKeys.includes(key as TProteumRequiredEnvVariableKey) ||
199
+ optionalProteumEnvVariablePrefixes.some((prefix) => key.startsWith(prefix)),
200
+ )
201
+ .sort((a, b) => a.localeCompare(b));
202
+
203
+ export const inspectProteumEnv = (appDir: string): TProteumEnvInspection => {
204
+ loadOptionalProteumDotenv(appDir);
205
+
206
+ return {
207
+ loadedVariableKeys: getLoadedProteumEnvVariableKeys(),
208
+ requiredVariables: requiredProteumEnvVariableKeys.map((key) => ({
209
+ key,
210
+ possibleValues: [...requiredProteumEnvVariablePossibleValues[key]],
211
+ provided: isProvidedEnvValue(process.env[key]),
212
+ })),
213
+ };
214
+ };
215
+
216
+ export const parseProteumEnvConfig = ({
217
+ appDir,
218
+ routerPortOverride,
219
+ }: {
220
+ appDir: string;
221
+ routerPortOverride?: number;
222
+ }): TProteumEnvConfig => {
223
+ loadOptionalProteumDotenv(appDir);
224
+
225
+ const name = parseEnvName(getRequiredEnvValue({ key: 'ENV_NAME', appDir }), appDir);
226
+ const profile = parseEnvProfile(getRequiredEnvValue({ key: 'ENV_PROFILE', appDir }), appDir);
227
+ const configuredRouterPort = parseIntegerEnvValue({
228
+ key: 'PORT',
229
+ value: getRequiredEnvValue({ key: 'PORT', appDir }),
230
+ appDir,
231
+ });
232
+ const currentDomain = parseAbsoluteUrl({
233
+ key: 'URL',
234
+ value: getRequiredEnvValue({ key: 'URL', appDir }),
235
+ appDir,
236
+ });
237
+
238
+ const traceEnable = parseBooleanEnvValue({
239
+ key: 'TRACE_ENABLE',
240
+ value: process.env.TRACE_ENABLE,
241
+ appDir,
242
+ });
243
+ const tracePersistOnError = parseBooleanEnvValue({
244
+ key: 'TRACE_PERSIST_ON_ERROR',
245
+ value: process.env.TRACE_PERSIST_ON_ERROR,
246
+ appDir,
247
+ });
248
+ const traceRequestsLimit = process.env.TRACE_REQUESTS_LIMIT?.trim();
249
+ const traceEventsLimit = process.env.TRACE_EVENTS_LIMIT?.trim();
250
+ const traceCapture = parseTraceCapture({
251
+ value: process.env.TRACE_CAPTURE?.trim(),
252
+ appDir,
253
+ });
254
+
255
+ return {
256
+ name,
257
+ profile,
258
+ router: {
259
+ port: routerPortOverride === undefined ? configuredRouterPort : routerPortOverride,
260
+ currentDomain,
261
+ },
262
+ trace: {
263
+ enable: traceEnable ?? profile === 'dev',
264
+ requestsLimit:
265
+ traceRequestsLimit === undefined || traceRequestsLimit === ''
266
+ ? 200
267
+ : parseIntegerEnvValue({
268
+ key: 'TRACE_REQUESTS_LIMIT',
269
+ value: traceRequestsLimit,
270
+ appDir,
271
+ }),
272
+ eventsLimit:
273
+ traceEventsLimit === undefined || traceEventsLimit === ''
274
+ ? 800
275
+ : parseIntegerEnvValue({
276
+ key: 'TRACE_EVENTS_LIMIT',
277
+ value: traceEventsLimit,
278
+ appDir,
279
+ }),
280
+ capture: traceCapture ?? 'resolve',
281
+ persistOnError: tracePersistOnError ?? profile === 'dev',
282
+ },
283
+ };
284
+ };
@@ -106,8 +106,6 @@ export type TRouteModule<TRegisteredRoute = any> = {
106
106
  __register: TAppArrowFunction<TRegisteredRoute>;
107
107
  };
108
108
 
109
- export type TDomainsList = { [endpointId: string]: string } & { current: string };
110
-
111
109
  export const defaultOptions: Pick<TRouteOptions, 'priority'> = { priority: 0 };
112
110
 
113
111
  /*----------------------------------
@@ -116,31 +114,15 @@ export const defaultOptions: Pick<TRouteOptions, 'priority'> = { priority: 0 };
116
114
  export const buildUrl = (
117
115
  path: string,
118
116
  params: { [key: string]: any },
119
- domains: { [alias: string]: string },
117
+ currentDomain: string,
120
118
  absolute: boolean,
121
119
  ) => {
122
120
  let prefix: string = '';
123
121
 
124
122
  // Relative to domain
125
- if (path[0] === '/' && absolute) prefix = domains.current;
126
- // Other domains of the project
127
- else if (path[0] === '@') {
128
- // Extract domain ID from path
129
- let domainId: string;
130
- let slackPos = path.indexOf('/');
131
- if (slackPos === -1) slackPos = path.length;
132
- domainId = path.substring(1, slackPos);
133
- path = path.substring(slackPos);
134
-
135
- // Get domain
136
- const domain = domains[domainId];
137
- if (domain === undefined) throw new Error('Unknown API endpoint ID: ' + domainId);
138
-
139
- // Return full url
140
- prefix = domain;
141
-
142
- // Absolute URL
143
- }
123
+ if (path[0] === '/' && absolute) prefix = currentDomain;
124
+ else if (path[0] === '@')
125
+ throw new Error(`Proteum no longer supports Router.url() domain aliases. Use a root-relative path or absolute URL instead: "${path}".`);
144
126
 
145
127
  // Path parapeters
146
128
  const searchParams = new URLSearchParams();
@@ -21,7 +21,7 @@ proteum trace latest --url http://127.0.0.1:3010
21
21
 
22
22
  Before reproducing a bug or starting a new test pass:
23
23
 
24
- - read the default port from `env.yaml`
24
+ - read the default port from `PORT` or `./.proteum/manifest.json`
25
25
  - check whether a dev server is already running on that port
26
26
  - if it is, inspect `proteum trace requests`, `proteum trace latest`, and `proteum trace show <requestId>` first so you can capture past errors and their context
27
27
 
@@ -60,15 +60,14 @@ Use `deep` selectively. It is for one-off investigation, not continuous capture.
60
60
 
61
61
  ## Configuration
62
62
 
63
- Add trace settings in `env.yaml`:
63
+ Set trace behavior with env vars:
64
64
 
65
- ```yaml
66
- trace:
67
- enable: true
68
- requestsLimit: 200
69
- eventsLimit: 800
70
- capture: resolve
71
- persistOnError: true
65
+ ```bash
66
+ export TRACE_ENABLE=true
67
+ export TRACE_REQUESTS_LIMIT=200
68
+ export TRACE_EVENTS_LIMIT=800
69
+ export TRACE_CAPTURE=resolve
70
+ export TRACE_PERSIST_ON_ERROR=true
72
71
  ```
73
72
 
74
73
  Notes:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "proteum",
3
3
  "description": "LLM-first Opinionated Typescript Framework for web applications.",
4
- "version": "2.1.0-4",
4
+ "version": "2.1.0-5",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/proteum.git",
7
7
  "license": "MIT",
@@ -12,10 +12,10 @@ import fs from 'fs-extra';
12
12
  import yaml from 'yaml';
13
13
 
14
14
  // Types
15
- import type { TDomainsList } from '@common/router';
16
- import type { TLogProfile } from './console';
15
+ import { parseProteumEnvConfig, type TProteumLoadedEnvConfig } from '../../../common/env/proteumEnv';
17
16
 
18
- declare const PROTEUM_ROUTER_PORT_OVERRIDE: number | null;
17
+ declare const PROTEUM_PORT_OVERRIDE: number | null;
18
+ declare const BUILD_DATE: string;
19
19
 
20
20
  /*----------------------------------
21
21
  - TYPES
@@ -31,50 +31,8 @@ declare global {
31
31
  }
32
32
  }
33
33
 
34
- /*
35
- name: server
36
- profile: prod
37
-
38
- router:
39
- port: 80
40
- domains:
41
- current: 'https://recruiters.becrosspath.com'
42
- recruiters: 'https://recruiters.becrosspath.com'
43
- landing: 'https://becrosspath.com'
44
- employers: 'https://employers.becrosspath.com'
45
- candidates: 'https://candidates.becrosspath.com'
46
- csm: 'https://csm.becrosspath.com'
47
-
48
- database:
49
- name: 'aws'
50
- databases: [railway]
51
- host: 'mysql-z7vp.railway.internal'
52
- port: 3306
53
- login: root
54
- password: "GMnVsczoyYkyzwvVqDkMUOAIjVsumEev"
55
-
56
- console:
57
- enable: false
58
- debug: false
59
- bufferLimit: 10000
60
- level: 'log'
61
- */
62
-
63
34
  export type TEnvName = TEnvConfig['name'];
64
- export type TEnvConfig = {
65
- name: 'local' | 'server';
66
- profile: 'dev' | 'testing' | 'prod';
67
-
68
- router: { port: number; domains: TDomainsList };
69
- console: { enable: boolean; debug: boolean; bufferLimit: number; level: TLogProfile };
70
- trace: {
71
- enable: boolean;
72
- requestsLimit: number;
73
- eventsLimit: number;
74
- capture: 'summary' | 'resolve' | 'deep';
75
- persistOnError: boolean;
76
- };
77
- };
35
+ export type TEnvConfig = TProteumLoadedEnvConfig;
78
36
 
79
37
  type AppIdentityConfig = {
80
38
  name: string;
@@ -105,8 +63,7 @@ export type AppConfig = { env: Config.Env; identity: Config.Identity };
105
63
  const debug = false;
106
64
 
107
65
  const getRouterPortOverride = () => {
108
- if (typeof PROTEUM_ROUTER_PORT_OVERRIDE !== 'undefined' && PROTEUM_ROUTER_PORT_OVERRIDE !== null)
109
- return PROTEUM_ROUTER_PORT_OVERRIDE;
66
+ if (typeof PROTEUM_PORT_OVERRIDE !== 'undefined' && PROTEUM_PORT_OVERRIDE !== null) return PROTEUM_PORT_OVERRIDE;
110
67
 
111
68
  return undefined;
112
69
  };
@@ -127,23 +84,13 @@ export default class ConfigParser {
127
84
  }
128
85
 
129
86
  public env(): TEnvConfig {
130
- // We assume that when we run 5htp dev, we're in local
131
- // Otherwise, we're in production environment (docker)
132
- debug && console.info('[app] Using environment:', process.env.NODE_ENV);
133
- const envFileName = this.appDir + '/env.yaml';
134
- const envFile = this.loadYaml(envFileName);
135
- const routerPortOverride = getRouterPortOverride();
136
- const traceConfig = envFile.trace || {};
87
+ debug && console.info('[app] Loading Proteum env vars from process.env');
88
+
137
89
  return {
138
- ...envFile,
139
- router: routerPortOverride === undefined ? envFile.router : { ...envFile.router, port: routerPortOverride },
140
- trace: {
141
- enable: traceConfig.enable ?? envFile.profile === 'dev',
142
- requestsLimit: traceConfig.requestsLimit ?? 200,
143
- eventsLimit: traceConfig.eventsLimit ?? 800,
144
- capture: traceConfig.capture ?? 'resolve',
145
- persistOnError: traceConfig.persistOnError ?? envFile.profile === 'dev',
146
- },
90
+ ...parseProteumEnvConfig({
91
+ appDir: this.appDir,
92
+ routerPortOverride: getRouterPortOverride(),
93
+ }),
147
94
  version: BUILD_DATE,
148
95
  };
149
96
  }
@@ -23,9 +23,10 @@ import type ServerRequest from '@server/services/router/request';
23
23
  - SERVICE CONFIG
24
24
  ----------------------------------*/
25
25
 
26
- export type TLogProfile = 'silly' | 'info' | 'warn' | 'error';
26
+ export type TLogProfile = 'silly' | 'log' | 'info' | 'warn' | 'error';
27
27
 
28
28
  export type Config = { debug?: boolean; enable: boolean; bufferLimit: number; level: TLogProfile };
29
+ export const defaultConsoleConfig: Config = { enable: false, debug: false, bufferLimit: 10000, level: 'log' };
29
30
 
30
31
  export type Hooks = {};
31
32
 
@@ -14,7 +14,7 @@ import type Application from '..';
14
14
  import type { StartedServicesIndex } from '../service';
15
15
  import Services, { ServicesContainer } from '../service/container';
16
16
  import ConfigParser, { TEnvConfig } from './config';
17
- import Console from './console';
17
+ import Console, { defaultConsoleConfig } from './console';
18
18
  import Trace from './trace';
19
19
  import type ServerRequest from '@server/services/router/request';
20
20
 
@@ -53,7 +53,7 @@ export class ApplicationContainer<TServicesIndex extends StartedServicesIndex =
53
53
  const configParser = new ConfigParser(this.path.root);
54
54
  this.Environment = configParser.env();
55
55
  this.Identity = configParser.identity();
56
- this.Console = new Console(this, this.Environment.console);
56
+ this.Console = new Console(this, defaultConsoleConfig);
57
57
  this.Trace = new Trace(this, this.Environment.trace);
58
58
  }
59
59
 
@@ -32,7 +32,6 @@ import BaseRouter, {
32
32
  defaultOptions,
33
33
  matchRoute,
34
34
  buildUrl,
35
- TDomainsList,
36
35
  } from '@common/router';
37
36
  import type { TSsrUnresolvedRoute, TRegisterPageArgs } from '@common/router/contracts';
38
37
  import { buildRegex, getRegisterPageArgs } from '@common/router/register';
@@ -108,7 +107,7 @@ export type Config<
108
107
 
109
108
  disk?: string; // Disk driver ID
110
109
 
111
- domains: TDomainsList;
110
+ currentDomain: string;
112
111
 
113
112
  http: HttpServiceConfig;
114
113
 
@@ -356,7 +355,7 @@ export default class ServerRouter<
356
355
  }
357
356
 
358
357
  public url = (path: string, params: {} = {}, absolute: boolean = true) =>
359
- buildUrl(path, params, this.config.domains, absolute);
358
+ buildUrl(path, params, this.config.currentDomain, absolute);
360
359
 
361
360
  /*----------------------------------
362
361
  - REGISTER
@@ -14,7 +14,7 @@ import express from 'express';
14
14
  import context from '@server/context';
15
15
  import type { AnyRouterService, default as ServerRouter, TServerRouter, TAnyRouter } from '@server/services/router';
16
16
  import ServerRequest from '@server/services/router/request';
17
- import { TMatchedRoute, TRoute, TAnyRoute, TDomainsList } from '@common/router';
17
+ import { TMatchedRoute, TRoute, TAnyRoute } from '@common/router';
18
18
  import { NotFound, Forbidden, Anomaly } from '@common/errors';
19
19
  import BaseResponse, { TResponseData } from '@common/router/response';
20
20
  import { splitRouteSetupResult } from '@common/router/pageSetup';
@@ -38,7 +38,7 @@ export type TBasicSSrData = {
38
38
  request: { data: TObjetDonnees; id: string };
39
39
  page: { chunkId: string; data?: TObjetDonnees };
40
40
  user: TBasicUser | null;
41
- domains: TDomainsList;
41
+ currentDomain: string;
42
42
  };
43
43
 
44
44
  type TServerRouterApplication<TRouter extends TServerRouter> =
@@ -277,7 +277,7 @@ export default class ServerResponse<
277
277
  request: { id: this.request.id, data: this.request.data },
278
278
  page: { chunkId: page.chunkId || '', data: page.data },
279
279
  user: this.request.user,
280
- domains: this.router.config.domains,
280
+ currentDomain: this.router.config.currentDomain,
281
281
  ...customSsrData,
282
282
  };
283
283
  }
@@ -43,24 +43,17 @@ declare type PrimitiveValue = string | number | boolean;
43
43
  /*type TEnvConfig = {
44
44
  name: 'local' | 'server',
45
45
  profile: 'dev' | 'testing' | 'prod',
46
-
46
+
47
47
  router: {
48
48
  port: number,
49
- domains: TDomainsList
50
- },
51
- database: {
52
- name: string,
53
- databases: string[],
54
- host: string,
55
- port: number,
56
- login: string,
57
- password: string,
49
+ currentDomain: string
58
50
  },
59
- console: {
51
+ trace: {
60
52
  enable: boolean,
61
- debug: boolean,
62
- bufferLimit: number,
63
- level: TLogProfile,
53
+ requestsLimit: number,
54
+ eventsLimit: number,
55
+ capture: 'summary' | 'resolve' | 'deep',
56
+ persistOnError: boolean,
64
57
  },
65
58
  }*/
66
59