@crouter/api 0.3.387 → 0.3.389

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.
Files changed (131) hide show
  1. package/dist/api/__tests__/integration/client.test.js +97 -0
  2. package/dist/api/client.d.ts +7 -0
  3. package/dist/api/client.js +40 -21
  4. package/dist/api/dto/config.d.ts +11 -1
  5. package/dist/core/asset-root.d.ts +7 -0
  6. package/dist/core/asset-root.js +18 -0
  7. package/dist/core/canvas/boot-id.d.ts +6 -0
  8. package/dist/core/canvas/boot-id.js +26 -0
  9. package/dist/core/canvas/paths.d.ts +72 -0
  10. package/dist/core/canvas/paths.js +163 -0
  11. package/dist/core/canvas/pid.d.ts +391 -0
  12. package/dist/core/canvas/pid.js +948 -0
  13. package/dist/core/command-plugins/bundle.d.ts +149 -0
  14. package/dist/core/command-plugins/bundle.js +588 -0
  15. package/dist/core/command-plugins/endpoint.d.ts +24 -0
  16. package/dist/core/command-plugins/endpoint.js +51 -0
  17. package/dist/core/config.d.ts +233 -0
  18. package/dist/core/config.js +1120 -0
  19. package/dist/core/env-name.d.ts +6 -0
  20. package/dist/core/env-name.js +9 -0
  21. package/dist/core/errors.d.ts +38 -0
  22. package/dist/core/errors.js +90 -0
  23. package/dist/core/events/emit.d.ts +6 -0
  24. package/dist/core/events/emit.js +42 -0
  25. package/dist/core/events/envelope.d.ts +2 -0
  26. package/dist/core/events/envelope.js +84 -0
  27. package/dist/core/events/errors.d.ts +4 -0
  28. package/dist/core/events/errors.js +69 -0
  29. package/dist/core/events/operation-id.d.ts +4 -0
  30. package/dist/core/events/operation-id.js +24 -0
  31. package/dist/core/events/serialize.d.ts +4 -0
  32. package/dist/core/events/serialize.js +199 -0
  33. package/dist/core/events/source.d.ts +16 -0
  34. package/dist/core/events/source.js +31 -0
  35. package/dist/core/events/types.d.ts +68 -0
  36. package/dist/core/events/types.js +11 -0
  37. package/dist/core/exclusive-lock.d.ts +34 -0
  38. package/dist/core/exclusive-lock.js +197 -0
  39. package/dist/core/fs-utils.d.ts +44 -0
  40. package/dist/core/fs-utils.js +208 -0
  41. package/dist/core/help.d.ts +309 -0
  42. package/dist/core/help.js +406 -0
  43. package/dist/core/human/page-catalog.d.ts +57 -0
  44. package/dist/core/human/page-catalog.js +172 -0
  45. package/dist/core/installed-plugins.d.ts +2 -0
  46. package/dist/core/installed-plugins.js +79 -0
  47. package/dist/core/io.d.ts +122 -0
  48. package/dist/core/io.js +373 -0
  49. package/dist/core/keybindings/attach-control.d.ts +49 -0
  50. package/dist/core/keybindings/attach-control.js +42 -0
  51. package/dist/core/keybindings/catalog.d.ts +18 -0
  52. package/dist/core/keybindings/catalog.js +257 -0
  53. package/dist/core/keybindings/types.d.ts +42 -0
  54. package/dist/core/keybindings/types.js +1 -0
  55. package/dist/core/layout.d.ts +26 -0
  56. package/dist/core/layout.js +94 -0
  57. package/dist/core/locked-file.d.ts +27 -0
  58. package/dist/core/locked-file.js +118 -0
  59. package/dist/core/log.d.ts +9 -0
  60. package/dist/core/log.js +89 -0
  61. package/dist/core/manifest.d.ts +5 -0
  62. package/dist/core/manifest.js +15 -0
  63. package/dist/core/plugin-env.d.ts +8 -0
  64. package/dist/core/plugin-env.js +31 -0
  65. package/dist/core/plugin-extensions.d.ts +29 -0
  66. package/dist/core/plugin-extensions.js +191 -0
  67. package/dist/core/plugin-swap-lock.d.ts +9 -0
  68. package/dist/core/plugin-swap-lock.js +31 -0
  69. package/dist/core/preview-result-path.d.ts +4 -0
  70. package/dist/core/preview-result-path.js +26 -0
  71. package/dist/core/profiles/env-store.d.ts +22 -0
  72. package/dist/core/profiles/env-store.js +163 -0
  73. package/dist/core/profiles/fuzzy-match.d.ts +19 -0
  74. package/dist/core/profiles/fuzzy-match.js +92 -0
  75. package/dist/core/profiles/manifest.d.ts +120 -0
  76. package/dist/core/profiles/manifest.js +529 -0
  77. package/dist/core/rate-limit-scope.d.ts +25 -0
  78. package/dist/core/rate-limit-scope.js +64 -0
  79. package/dist/core/render.d.ts +12 -0
  80. package/dist/core/render.js +138 -0
  81. package/dist/core/resolver.d.ts +14 -0
  82. package/dist/core/resolver.js +111 -0
  83. package/dist/core/runtime/branded-host.d.ts +25 -0
  84. package/dist/core/runtime/branded-host.js +264 -0
  85. package/dist/core/runtime/broker/daemon-ops.d.ts +65 -0
  86. package/dist/core/runtime/broker/daemon-ops.js +177 -0
  87. package/dist/core/runtime/broker/signal-stream.d.ts +30 -0
  88. package/dist/core/runtime/broker/signal-stream.js +149 -0
  89. package/dist/core/scope.d.ts +32 -0
  90. package/dist/core/scope.js +184 -0
  91. package/dist/core/scoped-state/db.d.ts +17 -0
  92. package/dist/core/scoped-state/db.js +247 -0
  93. package/dist/core/scoped-state/migrate.d.ts +8 -0
  94. package/dist/core/scoped-state/migrate.js +187 -0
  95. package/dist/core/scoped-state/paths.d.ts +9 -0
  96. package/dist/core/scoped-state/paths.js +27 -0
  97. package/dist/core/scoped-state/profiles.d.ts +27 -0
  98. package/dist/core/scoped-state/profiles.js +93 -0
  99. package/dist/core/scoped-state/providers.d.ts +24 -0
  100. package/dist/core/scoped-state/providers.js +19 -0
  101. package/dist/core/scoped-state/schema.d.ts +6 -0
  102. package/dist/core/scoped-state/schema.js +43 -0
  103. package/dist/core/scoped-state/settings.d.ts +28 -0
  104. package/dist/core/scoped-state/settings.js +83 -0
  105. package/dist/core/spaces/open-beneath.d.ts +71 -0
  106. package/dist/core/spaces/open-beneath.js +581 -0
  107. package/dist/core/sqlite-statements.d.ts +4 -0
  108. package/dist/core/sqlite-statements.js +17 -0
  109. package/dist/core/subscription-state.d.ts +121 -0
  110. package/dist/core/subscription-state.js +287 -0
  111. package/dist/core/user-settings.d.ts +377 -0
  112. package/dist/core/user-settings.js +458 -0
  113. package/dist/daemon/broker-signals/bus.d.ts +30 -0
  114. package/dist/daemon/broker-signals/bus.js +87 -0
  115. package/dist/daemon/manage.d.ts +176 -0
  116. package/dist/daemon/manage.js +664 -0
  117. package/dist/daemon/pidfile.d.ts +8 -0
  118. package/dist/daemon/pidfile.js +37 -0
  119. package/dist/daemon/startup-policy.d.ts +1 -0
  120. package/dist/daemon/startup-policy.js +1 -0
  121. package/dist/native/linux.d.ts +29 -0
  122. package/dist/native/linux.js +20 -0
  123. package/dist/shared/env.d.ts +116 -0
  124. package/dist/shared/env.js +271 -0
  125. package/dist/shared/inbox-entry-body.d.ts +22 -0
  126. package/dist/shared/inbox-entry-body.js +116 -0
  127. package/dist/shared/working-activity.d.ts +9 -0
  128. package/dist/shared/working-activity.js +27 -0
  129. package/dist/types.d.ts +562 -0
  130. package/dist/types.js +186 -0
  131. package/package.json +1 -1
@@ -0,0 +1,6 @@
1
+ export interface EnvNameValidation {
2
+ isValid: boolean;
3
+ errors: string[];
4
+ }
5
+ /** Validate an environment variable identifier without reading its value. */
6
+ export declare function validateEnvVarName(name: unknown): EnvNameValidation;
@@ -0,0 +1,9 @@
1
+ /** Validate an environment variable identifier without reading its value. */
2
+ export function validateEnvVarName(name) {
3
+ const errors = [];
4
+ if (typeof name !== 'string')
5
+ errors.push('env var name must be a string');
6
+ else if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
7
+ errors.push('env var name must start with letter/underscore, followed by alphanumeric/underscore');
8
+ return { isValid: errors.length === 0, errors };
9
+ }
@@ -0,0 +1,38 @@
1
+ import { type ExitCodeValue } from '../types.js';
2
+ export declare class CrtrError extends Error {
3
+ code: string;
4
+ exitCode: ExitCodeValue;
5
+ details?: Record<string, unknown>;
6
+ constructor(code: string, message: string, exitCode?: ExitCodeValue, details?: Record<string, unknown>);
7
+ }
8
+ export declare function notFound(message: string, details?: Record<string, unknown>): CrtrError;
9
+ export declare function usage(message: string, details?: Record<string, unknown>): CrtrError;
10
+ export declare function ambiguous(message: string, details?: Record<string, unknown>): CrtrError;
11
+ export declare function network(message: string, details?: Record<string, unknown>): CrtrError;
12
+ export declare function general(message: string, details?: Record<string, unknown>): CrtrError;
13
+ /** A node's broker ENGINE failed to launch or bind its view socket — an
14
+ * operational failure surfaced from deep in the spawn/revive path (e.g. the
15
+ * front-door root create). It is NOT a bad request and NOT an internal crtr
16
+ * bug: it carries a curated cause the caller needs, so it must never collapse
17
+ * into a bare `internal` 500 with the message stripped.
18
+ *
19
+ * The `details` deliberately carry the FULL `{error, message, next}` payload so
20
+ * a server-thrown instance round-trips through crtrd verbatim: `toErrorBody`
21
+ * surfaces it as a <500 body with structured details, and the client's
22
+ * `apiErrorToCliError` (tier 2) reconstructs the exact code, message, and
23
+ * actionable `next` instead of degrading to a generic `invalid_request`. */
24
+ export declare function brokerLaunchFailed(message: string, next: string): CrtrError;
25
+ export declare function nodeCreateRefused(message: string, next: string): CrtrError;
26
+ /** Refuse, inside an app's sandboxed node, something only the person's own
27
+ * terminal can reach (their stores, settings, or remote canvas targets) with
28
+ * the daemon-operations `owner_only` refusal instead of an internal error
29
+ * from touching storage the sandbox does not mount. */
30
+ export declare function ownerOnly(reason: string, next: string): CrtrError;
31
+ /** Refuse a CLI capability that was omitted from this run's allow-list with
32
+ * the daemon-operations `scope_missing` refusal naming the missing string.
33
+ * `scope` is an identity scope string (`crtr:act`); a bare capability name
34
+ * (`act`) is read as `crtr:<name>` so it can never reach `covers()` unparsed. */
35
+ export declare function requireScope(scope: string, command: string): void;
36
+ /** Thrown by stub handlers for leaves not yet wired in P3+.
37
+ * code='not_implemented', exitCode=GENERAL, next names the node. */
38
+ export declare function notImplemented(node: string): CrtrError;
@@ -0,0 +1,90 @@
1
+ import { ExitCode } from '../types.js';
2
+ import { envScopes, scopeAllowed } from '../shared/env.js';
3
+ export class CrtrError extends Error {
4
+ code;
5
+ exitCode;
6
+ details;
7
+ constructor(code, message, exitCode = ExitCode.GENERAL, details) {
8
+ super(message);
9
+ this.name = 'CrtrError';
10
+ this.code = code;
11
+ this.exitCode = exitCode;
12
+ this.details = details;
13
+ }
14
+ }
15
+ export function notFound(message, details) {
16
+ return new CrtrError('not_found', message, ExitCode.NOT_FOUND, details);
17
+ }
18
+ export function usage(message, details) {
19
+ return new CrtrError('usage', message, ExitCode.USAGE, details);
20
+ }
21
+ export function ambiguous(message, details) {
22
+ return new CrtrError('ambiguous', message, ExitCode.AMBIGUOUS, details);
23
+ }
24
+ export function network(message, details) {
25
+ return new CrtrError('network', message, ExitCode.NETWORK, details);
26
+ }
27
+ export function general(message, details) {
28
+ return new CrtrError('error', message, ExitCode.GENERAL, details);
29
+ }
30
+ /** A node's broker ENGINE failed to launch or bind its view socket — an
31
+ * operational failure surfaced from deep in the spawn/revive path (e.g. the
32
+ * front-door root create). It is NOT a bad request and NOT an internal crtr
33
+ * bug: it carries a curated cause the caller needs, so it must never collapse
34
+ * into a bare `internal` 500 with the message stripped.
35
+ *
36
+ * The `details` deliberately carry the FULL `{error, message, next}` payload so
37
+ * a server-thrown instance round-trips through crtrd verbatim: `toErrorBody`
38
+ * surfaces it as a <500 body with structured details, and the client's
39
+ * `apiErrorToCliError` (tier 2) reconstructs the exact code, message, and
40
+ * actionable `next` instead of degrading to a generic `invalid_request`. */
41
+ export function brokerLaunchFailed(message, next) {
42
+ const code = 'broker_launch_failed';
43
+ return new CrtrError(code, message, ExitCode.GENERAL, { error: code, message, next });
44
+ }
45
+ export function nodeCreateRefused(message, next) {
46
+ const code = 'node_create_refused';
47
+ return new CrtrError(code, message, ExitCode.GENERAL, { error: code, message, next });
48
+ }
49
+ /** Refuse, inside an app's sandboxed node, something only the person's own
50
+ * terminal can reach (their stores, settings, or remote canvas targets) with
51
+ * the daemon-operations `owner_only` refusal instead of an internal error
52
+ * from touching storage the sandbox does not mount. */
53
+ export function ownerOnly(reason, next) {
54
+ const code = 'owner_only';
55
+ const message = `${reason}; only the person's terminal can.`;
56
+ return new CrtrError(code, message, ExitCode.USAGE, { error: code, message, next });
57
+ }
58
+ /** Refuse a CLI capability that was omitted from this run's allow-list with
59
+ * the daemon-operations `scope_missing` refusal naming the missing string.
60
+ * `scope` is an identity scope string (`crtr:act`); a bare capability name
61
+ * (`act`) is read as `crtr:<name>` so it can never reach `covers()` unparsed. */
62
+ export function requireScope(scope, command) {
63
+ const required = scope.startsWith('crtr:') ? scope : `crtr:${scope}`;
64
+ const scopes = envScopes();
65
+ // A held entry that is not a parseable scope string covers nothing; it must
66
+ // not hide a valid entry later in the list.
67
+ const allowed = scopes === null || scopes === undefined || scopes.some((held) => {
68
+ try {
69
+ return scopeAllowed([held], required);
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ });
75
+ if (allowed)
76
+ return;
77
+ const code = 'scope_missing';
78
+ const message = `missing scope ${required}: this run was not launched with it, so \`${command}\` is not available.`;
79
+ throw new CrtrError(code, message, ExitCode.GENERAL, {
80
+ error: code,
81
+ message,
82
+ scope: required,
83
+ next: 'This capability was not granted to the run. Do not retry it — proceed without it and say so in your result.',
84
+ });
85
+ }
86
+ /** Thrown by stub handlers for leaves not yet wired in P3+.
87
+ * code='not_implemented', exitCode=GENERAL, next names the node. */
88
+ export function notImplemented(node) {
89
+ return new CrtrError('not_implemented', `${node} is not yet implemented.`, ExitCode.GENERAL, { next: 'This leaf is not yet wired.' });
90
+ }
@@ -0,0 +1,6 @@
1
+ import type { BuildEnvelopeInput } from './types.js';
2
+ export type EmitEventInput = Omit<BuildEnvelopeInput, 'component' | 'stream_id'>;
3
+ /** Resolves after every broker event emitted so far has been appended or written to stderr. */
4
+ export declare function flushBrokerLog(): Promise<void>;
5
+ /** The only canonical write path. The bound source owns the destination component/path/stream; node_id identifies the event target, with a broker source enforcing its bound node. */
6
+ export declare function emitEvent(input: EmitEventInput): boolean;
@@ -0,0 +1,42 @@
1
+ import { appendBufferedDaemonRecord, appendRotatingRecord } from '../log.js';
2
+ import { buildEnvelope } from './envelope.js';
3
+ import { serializeEventRecord } from './serialize.js';
4
+ import { eventSource } from './source.js';
5
+ import { envExecutionId } from '../../shared/env.js';
6
+ import { appendBrokerLog } from '../runtime/broker/daemon-ops.js';
7
+ // A launched broker cannot open its node's records, so its events append through
8
+ // the daemon in emit order. A record the daemon did not append goes to stderr,
9
+ // which the daemon-opened broker.log fd captures.
10
+ let brokerLogTail = Promise.resolve();
11
+ /** Resolves after every broker event emitted so far has been appended or written to stderr. */
12
+ export function flushBrokerLog() { return brokerLogTail; }
13
+ function queueBrokerRecord(nodeId, record) {
14
+ brokerLogTail = brokerLogTail.then(async () => {
15
+ try {
16
+ const { appended } = await appendBrokerLog(nodeId, record);
17
+ if (!appended)
18
+ process.stderr.write(`[broker] log.jsonl refused record: ${record}\n`);
19
+ }
20
+ catch (error) {
21
+ process.stderr.write(`[broker] log.jsonl append failed (${error instanceof Error ? error.message : String(error)}): ${record}\n`);
22
+ }
23
+ });
24
+ }
25
+ /** The only canonical write path. The bound source owns the destination component/path/stream; node_id identifies the event target, with a broker source enforcing its bound node. */
26
+ export function emitEvent(input) {
27
+ const source = eventSource();
28
+ if (source === undefined)
29
+ return false;
30
+ if (source.component === 'broker' && source.node_id !== undefined && input.node_id !== undefined && input.node_id !== source.node_id) {
31
+ throw new TypeError(`broker event target node_id must match bound node_id; received ${input.node_id}`);
32
+ }
33
+ if (input.level === 'debug' && process.env['CRTR_LOG'] !== 'debug')
34
+ return false;
35
+ const envelope = buildEnvelope({ ...input, component: source.component, ...(source.node_id === undefined ? {} : { node_id: source.node_id }), ...(source.stream_id === undefined ? {} : { stream_id: source.stream_id }) });
36
+ const record = serializeEventRecord(envelope);
37
+ if (source.component === 'broker' && source.node_id !== undefined && envExecutionId() !== undefined) {
38
+ queueBrokerRecord(source.node_id, record);
39
+ return true;
40
+ }
41
+ return source.component === 'daemon' ? appendBufferedDaemonRecord(source.path, record) : appendRotatingRecord(source.path, record);
42
+ }
@@ -0,0 +1,2 @@
1
+ import type { BuildEnvelopeInput, EventEnvelope } from './types.js';
2
+ export declare function buildEnvelope(input: BuildEnvelopeInput): EventEnvelope;
@@ -0,0 +1,84 @@
1
+ import { errorClassCodes, eventErrorFromUnknown } from './errors.js';
2
+ import { generateOperationId, operationIdContext, validateOperationId } from './operation-id.js';
3
+ const eventName = /^[a-z][a-z0-9]*(?:[._][a-z0-9]+)*$/;
4
+ const levels = new Set(['error', 'warn', 'info', 'debug']);
5
+ const components = new Set(['daemon', 'broker', 'control-plane', 'front', 'sentinel']);
6
+ const outcomes = new Set(['succeeded', 'failed', 'skipped']);
7
+ const dispositions = new Set(['auto', 'manual', 'fatal']);
8
+ const classes = new Set(errorClassCodes);
9
+ function bytes(value) { return Buffer.byteLength(value, 'utf8'); }
10
+ function requireString(value, name, cap = 128) {
11
+ if (typeof value !== 'string' || value === '' || bytes(value) > cap)
12
+ throw new TypeError(`${name} must be a non-empty string of at most ${cap} UTF-8 bytes`);
13
+ return value;
14
+ }
15
+ function normalizedTimestamp(value) {
16
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T/.test(value))
17
+ throw new TypeError('ts must be an ISO 8601 timestamp');
18
+ const date = new Date(value);
19
+ if (Number.isNaN(date.getTime()))
20
+ throw new TypeError('ts must be an ISO 8601 timestamp');
21
+ return date.toISOString();
22
+ }
23
+ function checkedErrorClass(value) {
24
+ if (!classes.has(value.class))
25
+ throw new TypeError('error_class.class is invalid');
26
+ const result = { class: value.class };
27
+ if (value.status !== undefined) {
28
+ if (!Number.isInteger(value.status))
29
+ throw new TypeError('error_class.status must be an integer');
30
+ result.status = value.status;
31
+ }
32
+ if (value.provider !== undefined)
33
+ result.provider = requireString(value.provider, 'error_class.provider');
34
+ if (value.retry_disposition !== undefined) {
35
+ if (!dispositions.has(value.retry_disposition))
36
+ throw new TypeError('error_class.retry_disposition is invalid');
37
+ result.retry_disposition = value.retry_disposition;
38
+ }
39
+ return result;
40
+ }
41
+ export function buildEnvelope(input) {
42
+ if (!levels.has(input.level))
43
+ throw new TypeError('level is invalid');
44
+ if (!components.has(input.component))
45
+ throw new TypeError('component is invalid');
46
+ const event = requireString(input.event, 'event');
47
+ if (!eventName.test(event))
48
+ throw new TypeError('event must be a lowercase dotted identifier');
49
+ const operationId = input.operation_id === undefined ? operationIdContext.current() ?? generateOperationId() : validateOperationId(input.operation_id);
50
+ const result = {
51
+ v: 1,
52
+ ts: input.ts === undefined ? new Date().toISOString() : normalizedTimestamp(input.ts),
53
+ level: input.level,
54
+ event,
55
+ component: input.component,
56
+ operation_id: operationId,
57
+ };
58
+ if (input.node_id !== undefined)
59
+ result.node_id = requireString(input.node_id, 'node_id');
60
+ if (input.stream_id !== undefined)
61
+ result.stream_id = requireString(input.stream_id, 'stream_id');
62
+ if (input.duration_ms !== undefined) {
63
+ if (!Number.isFinite(input.duration_ms) || input.duration_ms < 0)
64
+ throw new TypeError('duration_ms must be finite and non-negative');
65
+ result.duration_ms = input.duration_ms;
66
+ }
67
+ if (input.outcome !== undefined) {
68
+ if (!outcomes.has(input.outcome))
69
+ throw new TypeError('outcome is invalid');
70
+ result.outcome = input.outcome;
71
+ }
72
+ if (input.attempt !== undefined) {
73
+ if (!Number.isInteger(input.attempt) || input.attempt <= 0)
74
+ throw new TypeError('attempt must be a positive integer');
75
+ result.attempt = input.attempt;
76
+ }
77
+ if (input.error_class !== undefined)
78
+ result.error_class = checkedErrorClass(input.error_class);
79
+ if (input.error !== undefined)
80
+ result.error = eventErrorFromUnknown(input.error);
81
+ if (input.fields !== undefined)
82
+ result.fields = input.fields;
83
+ return result;
84
+ }
@@ -0,0 +1,4 @@
1
+ import { errorClassCodes, type ErrorClass, type ErrorClassHints, type EventError } from './types.js';
2
+ export declare function errorClassFromError(error: unknown, hints?: ErrorClassHints): ErrorClass;
3
+ export declare function eventErrorFromUnknown(error: unknown): EventError;
4
+ export { errorClassCodes };
@@ -0,0 +1,69 @@
1
+ import { errorClassCodes } from './types.js';
2
+ const retryDispositions = new Set(['auto', 'manual', 'fatal']);
3
+ function record(value) {
4
+ return typeof value === 'object' && value !== null ? value : undefined;
5
+ }
6
+ function text(value) {
7
+ return typeof value === 'string' ? value : '';
8
+ }
9
+ function statusFrom(error) {
10
+ const value = record(error)?.['status'];
11
+ return typeof value === 'number' && Number.isInteger(value) ? value : undefined;
12
+ }
13
+ function codeFrom(error) {
14
+ const value = record(error);
15
+ return text(value?.['code']) || text(value?.['errno']);
16
+ }
17
+ function messageFrom(error) {
18
+ if (typeof error === 'string')
19
+ return error;
20
+ if (error instanceof Error)
21
+ return error.message;
22
+ const value = record(error);
23
+ return text(value?.['message']) || text(value?.['statusText']) || text(value?.['reason']);
24
+ }
25
+ function classify(status, code, message) {
26
+ if (status === 429 || /rate.?limit|too many requests|quota/i.test(message))
27
+ return 'rate_limit';
28
+ if (status === 401 || status === 403 || /invalid_grant|refresh token|unauthori[sz]ed|invalid.?api.?key|authentication failed/i.test(message))
29
+ return 'auth';
30
+ if (/context overflow/i.test(message))
31
+ return 'context_overflow';
32
+ if (/model.{0,40}(not found|does not exist|not available)|not_found_error/i.test(message))
33
+ return 'model_not_found';
34
+ if (/wedged/i.test(message))
35
+ return 'wedged';
36
+ if (/^(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOENT|ENOTFOUND|EAI_AGAIN|EPIPE|EHOSTUNREACH|ENETUNREACH)$/i.test(code) || /connection|econnreset|etimedout|enotfound|network|fetch failed|socket hang|timed? out|websocket.*(?:closed|error)/i.test(message))
37
+ return 'connection';
38
+ if ((status !== undefined && status >= 500) || /overloaded|capacity|server.{0,3}busy|temporarily unavailable|internal server error/i.test(message))
39
+ return 'overloaded';
40
+ if ((status !== undefined && status >= 400) || /protocol|invalid (?:frame|payload)|policy violation/i.test(message))
41
+ return 'protocol';
42
+ return 'unknown';
43
+ }
44
+ export function errorClassFromError(error, hints = {}) {
45
+ const status = hints.status ?? statusFrom(error);
46
+ const result = { class: classify(status, codeFrom(error), messageFrom(error)) };
47
+ if (status !== undefined)
48
+ result.status = status;
49
+ if (typeof hints.provider === 'string' && hints.provider !== '')
50
+ result.provider = hints.provider;
51
+ if (hints.retry_disposition !== undefined && retryDispositions.has(hints.retry_disposition))
52
+ result.retry_disposition = hints.retry_disposition;
53
+ return result;
54
+ }
55
+ export function eventErrorFromUnknown(error) {
56
+ if (error instanceof Error) {
57
+ const result = { type: error.name || 'Error', message: error.message };
58
+ if (typeof error.stack === 'string')
59
+ result.stack = error.stack;
60
+ if (error.cause !== undefined)
61
+ result.cause = error.cause instanceof Error ? error.cause.message : String(error.cause);
62
+ return result;
63
+ }
64
+ if (typeof error === 'string')
65
+ return { type: 'Error', message: error };
66
+ const value = record(error);
67
+ return { type: text(value?.['name']) || 'Error', message: text(value?.['message']) || String(error) };
68
+ }
69
+ export { errorClassCodes };
@@ -0,0 +1,4 @@
1
+ import type { OperationId, OperationIdContext } from './types.js';
2
+ export declare function validateOperationId(value: unknown): OperationId;
3
+ export declare function generateOperationId(): OperationId;
4
+ export declare const operationIdContext: OperationIdContext;
@@ -0,0 +1,24 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { randomBytes } from 'node:crypto';
3
+ const OPERATION_ID = /^(?!0{32}$)[0-9a-f]{32}$/;
4
+ const storage = new AsyncLocalStorage();
5
+ export function validateOperationId(value) {
6
+ if (typeof value !== 'string' || !OPERATION_ID.test(value))
7
+ throw new TypeError('operation_id must be 32 lowercase non-zero hexadecimal characters');
8
+ return value;
9
+ }
10
+ export function generateOperationId() {
11
+ let value;
12
+ do
13
+ value = randomBytes(16).toString('hex');
14
+ while (value === '00000000000000000000000000000000');
15
+ return validateOperationId(value);
16
+ }
17
+ export const operationIdContext = {
18
+ current: () => storage.getStore(),
19
+ run: (operationId, callback) => storage.run(validateOperationId(operationId), callback),
20
+ fresh: (callback) => {
21
+ const operationId = generateOperationId();
22
+ return storage.run(operationId, () => callback(operationId));
23
+ },
24
+ };
@@ -0,0 +1,4 @@
1
+ import type { EventEnvelope } from './types.js';
2
+ export declare const MAX_EVENT_RECORD_BYTES = 65536;
3
+ /** Serializes a bounded NDJSON record without its newline; appendRotatingRecord owns that byte. */
4
+ export declare function serializeEventRecord(envelope: EventEnvelope): string;
@@ -0,0 +1,199 @@
1
+ export const MAX_EVENT_RECORD_BYTES = 65_536;
2
+ const MAX_STRING_BYTES = 4_096;
3
+ const MAX_KEY_BYTES = 128;
4
+ const MAX_ERROR_STACK_BYTES = 16_384;
5
+ const MAX_DEPTH = 4;
6
+ const MAX_MEMBERS = 64;
7
+ // Bounds total normalize() work regardless of graph shape: MAX_DEPTH/MAX_MEMBERS
8
+ // alone permit a tiny SHARED-reference graph (no cycle, so the WeakSet guard
9
+ // never fires) to fan out combinatorially — e.g. four levels of 64 shared
10
+ // children reach 64^4 visits. This budget caps total node visits so one event
11
+ // cannot stall or exhaust its process regardless of how references are shared.
12
+ const MAX_NORMALIZE_UNITS = 5_000;
13
+ function byteLength(value) { return Buffer.byteLength(value, 'utf8'); }
14
+ function truncate(value, cap) {
15
+ if (byteLength(value) <= cap)
16
+ return { value, altered: false };
17
+ const suffix = '…';
18
+ const target = cap - byteLength(suffix);
19
+ let used = 0;
20
+ let result = '';
21
+ for (const character of value) {
22
+ const size = byteLength(character);
23
+ if (used + size > target)
24
+ break;
25
+ result += character;
26
+ used += size;
27
+ }
28
+ return { value: result + suffix, altered: true };
29
+ }
30
+ function unsupported(value) {
31
+ if (value === undefined)
32
+ return '[Unsupported undefined]';
33
+ if (typeof value === 'function')
34
+ return `[Unsupported function${value.name ? ` ${value.name}` : ''}]`;
35
+ if (typeof value === 'symbol')
36
+ return `[Unsupported ${String(value)}]`;
37
+ return `[Unsupported ${value?.constructor?.name ?? typeof value}]`;
38
+ }
39
+ function normalize(value, depth, active, budget) {
40
+ if (budget.remaining <= 0)
41
+ return { value: '[Omitted]', altered: true, dropped: 0 };
42
+ budget.remaining -= 1;
43
+ if (value === null || typeof value === 'boolean')
44
+ return { value, altered: false, dropped: 0 };
45
+ if (typeof value === 'string') {
46
+ const limited = truncate(value, MAX_STRING_BYTES);
47
+ return { value: limited.value, altered: limited.altered, dropped: 0 };
48
+ }
49
+ if (typeof value === 'number')
50
+ return Number.isFinite(value) ? { value, altered: false, dropped: 0 } : { value: String(value), altered: true, dropped: 0 };
51
+ if (typeof value === 'bigint')
52
+ return { value: value.toString(), altered: true, dropped: 0 };
53
+ if (value instanceof Date) {
54
+ const rendered = Number.isNaN(value.getTime()) ? 'Invalid Date' : value.toISOString();
55
+ return { value: rendered, altered: true, dropped: 0 };
56
+ }
57
+ if (typeof value !== 'object' || value === undefined)
58
+ return { value: unsupported(value), altered: true, dropped: 0 };
59
+ if (active.has(value))
60
+ return { value: '[Circular]', altered: true, dropped: 0 };
61
+ if (depth >= MAX_DEPTH)
62
+ return { value: '[Max depth]', altered: true, dropped: 0 };
63
+ active.add(value);
64
+ try {
65
+ if (Array.isArray(value)) {
66
+ const count = Math.min(value.length, MAX_MEMBERS);
67
+ const items = [];
68
+ let altered = value.length > count;
69
+ let dropped = value.length - count;
70
+ for (let index = 0; index < count; index += 1) {
71
+ if (budget.remaining <= 0) {
72
+ altered = true;
73
+ dropped += count - index;
74
+ break;
75
+ }
76
+ const item = normalize(value[index], depth + 1, active, budget);
77
+ items.push(item.value);
78
+ altered ||= item.altered;
79
+ dropped += item.dropped;
80
+ }
81
+ return { value: items, altered, dropped };
82
+ }
83
+ let keys;
84
+ try {
85
+ keys = Object.keys(value).sort();
86
+ }
87
+ catch {
88
+ return { value: '[Unsupported object]', altered: true, dropped: 0 };
89
+ }
90
+ const selected = keys.slice(0, MAX_MEMBERS);
91
+ const output = {};
92
+ let altered = keys.length > selected.length;
93
+ let dropped = keys.length - selected.length;
94
+ for (let index = 0; index < selected.length; index += 1) {
95
+ const key = selected[index];
96
+ if (budget.remaining <= 0) {
97
+ altered = true;
98
+ dropped += selected.length - index;
99
+ break;
100
+ }
101
+ const limitedKey = truncate(key, MAX_KEY_BYTES);
102
+ let child;
103
+ try {
104
+ child = normalize(value[key], depth + 1, active, budget);
105
+ }
106
+ catch {
107
+ child = { value: '[Unsupported property]', altered: true, dropped: 0 };
108
+ }
109
+ // Two distinct original keys can truncate to the same bounded key (e.g. two
110
+ // 200-byte keys sharing their first 127 bytes). Silently overwriting would
111
+ // under-report the omission; count the collision as dropped instead.
112
+ if (Object.prototype.hasOwnProperty.call(output, limitedKey.value)) {
113
+ altered = true;
114
+ dropped += 1;
115
+ continue;
116
+ }
117
+ output[limitedKey.value] = child.value;
118
+ altered ||= limitedKey.altered || child.altered;
119
+ dropped += child.dropped;
120
+ }
121
+ return { value: output, altered, dropped };
122
+ }
123
+ finally {
124
+ active.delete(value);
125
+ }
126
+ }
127
+ function normalizeError(error) {
128
+ const type = truncate(String(error.type), MAX_KEY_BYTES);
129
+ const message = truncate(String(error.message), MAX_STRING_BYTES);
130
+ const result = { type: type.value, message: message.value };
131
+ let altered = type.altered || message.altered;
132
+ if (error.stack !== undefined) {
133
+ const stack = truncate(String(error.stack), MAX_ERROR_STACK_BYTES);
134
+ result.stack = stack.value;
135
+ altered ||= stack.altered;
136
+ }
137
+ if (error.cause !== undefined) {
138
+ const cause = truncate(String(error.cause), MAX_STRING_BYTES);
139
+ result.cause = cause.value;
140
+ altered ||= cause.altered;
141
+ }
142
+ return { value: result, altered };
143
+ }
144
+ function encoded(value) { return JSON.stringify(value); }
145
+ function recordBytes(value) { return byteLength(encoded(value)) + 1; }
146
+ /** Serializes a bounded NDJSON record without its newline; appendRotatingRecord owns that byte. */
147
+ export function serializeEventRecord(envelope) {
148
+ const normalizedFields = envelope.fields === undefined ? undefined : normalize(envelope.fields, 0, new WeakSet(), { remaining: MAX_NORMALIZE_UNITS });
149
+ const normalizedError = envelope.error === undefined ? undefined : normalizeError(envelope.error);
150
+ const record = {
151
+ v: 1,
152
+ ts: envelope.ts,
153
+ level: envelope.level,
154
+ event: envelope.event,
155
+ component: envelope.component,
156
+ operation_id: envelope.operation_id,
157
+ ...(envelope.node_id === undefined ? {} : { node_id: envelope.node_id }),
158
+ ...(envelope.stream_id === undefined ? {} : { stream_id: envelope.stream_id }),
159
+ ...(envelope.duration_ms === undefined ? {} : { duration_ms: envelope.duration_ms }),
160
+ ...(envelope.outcome === undefined ? {} : { outcome: envelope.outcome }),
161
+ ...(envelope.attempt === undefined ? {} : { attempt: envelope.attempt }),
162
+ ...(envelope.error_class === undefined ? {} : { error_class: envelope.error_class }),
163
+ ...(normalizedError === undefined ? {} : { error: normalizedError.value }),
164
+ ...(normalizedFields === undefined ? {} : { fields: normalizedFields.value }),
165
+ };
166
+ const originalBytes = recordBytes(record);
167
+ const altered = normalizedFields?.altered === true || normalizedError?.altered === true || originalBytes > MAX_EVENT_RECORD_BYTES;
168
+ let dropped = normalizedFields?.dropped ?? 0;
169
+ if (altered)
170
+ Object.assign(record, { record_truncated: true, original_bytes: originalBytes, dropped_fields_count: dropped });
171
+ while (recordBytes(record) > MAX_EVENT_RECORD_BYTES && record['fields'] !== undefined) {
172
+ const fields = record['fields'];
173
+ const keys = Object.keys(fields);
174
+ if (keys.length === 0) {
175
+ delete record['fields'];
176
+ break;
177
+ }
178
+ keys.sort((left, right) => {
179
+ const bySize = byteLength(encoded(fields[right])) - byteLength(encoded(fields[left]));
180
+ return bySize !== 0 ? bySize : right < left ? -1 : right > left ? 1 : 0;
181
+ });
182
+ delete fields[keys[0]];
183
+ dropped += 1;
184
+ record.record_truncated = true;
185
+ record.original_bytes = originalBytes;
186
+ record.dropped_fields_count = dropped;
187
+ }
188
+ const error = record['error'];
189
+ if (recordBytes(record) > MAX_EVENT_RECORD_BYTES && error?.stack !== undefined)
190
+ delete error.stack;
191
+ if (recordBytes(record) > MAX_EVENT_RECORD_BYTES && error?.cause !== undefined)
192
+ delete error.cause;
193
+ if (recordBytes(record) > MAX_EVENT_RECORD_BYTES && error !== undefined)
194
+ error.message = truncate(error.message, 64).value;
195
+ const result = encoded(record);
196
+ if (byteLength(result) + 1 > MAX_EVENT_RECORD_BYTES)
197
+ throw new RangeError('event record cannot fit within 64 KiB');
198
+ return result;
199
+ }
@@ -0,0 +1,16 @@
1
+ import type { EventComponent } from './types.js';
2
+ export interface BoundEventSource {
3
+ component: EventComponent;
4
+ path: string;
5
+ node_id?: string;
6
+ stream_id?: string;
7
+ }
8
+ export declare function bindDaemonEventSource(): void;
9
+ export declare function bindBrokerEventSource(nodeId: string): void;
10
+ export declare function eventSource(): BoundEventSource | undefined;
11
+ /** Test-only: clear the process-global binding. A single test process can
12
+ * legitimately play several components in sequence (an in-process revive binds
13
+ * a broker source via host.ts's bind-if-unbound; a later daemon-tick test then
14
+ * inherits it and trips emit's cross-node guard). Production processes bind
15
+ * exactly once and never reset. */
16
+ export declare function resetEventSourceForTests(): void;
@@ -0,0 +1,31 @@
1
+ import { join } from 'node:path';
2
+ import { crtrHome, isSafeNodeId, jobDir } from '../canvas/paths.js';
3
+ let bound;
4
+ function equal(left, right) {
5
+ return left.component === right.component && left.path === right.path && left.node_id === right.node_id && left.stream_id === right.stream_id;
6
+ }
7
+ function bind(source) {
8
+ if (bound === undefined) {
9
+ bound = source;
10
+ return;
11
+ }
12
+ if (!equal(bound, source))
13
+ throw new Error('event source is already bound for this process');
14
+ }
15
+ export function bindDaemonEventSource() {
16
+ bind({ component: 'daemon', path: join(crtrHome(), 'crtrd.log') });
17
+ }
18
+ export function bindBrokerEventSource(nodeId) {
19
+ if (!isSafeNodeId(nodeId))
20
+ throw new TypeError('node_id must be a non-empty path segment of at most 128 UTF-8 bytes');
21
+ bind({ component: 'broker', node_id: nodeId, path: join(jobDir(nodeId), 'log.jsonl') });
22
+ }
23
+ export function eventSource() { return bound; }
24
+ /** Test-only: clear the process-global binding. A single test process can
25
+ * legitimately play several components in sequence (an in-process revive binds
26
+ * a broker source via host.ts's bind-if-unbound; a later daemon-tick test then
27
+ * inherits it and trips emit's cross-node guard). Production processes bind
28
+ * exactly once and never reset. */
29
+ export function resetEventSourceForTests() {
30
+ bound = undefined;
31
+ }