@ontrails/core 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +849 -0
  2. package/README.md +190 -0
  3. package/package.json +36 -0
  4. package/src/activation-provenance.ts +116 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-derivation.ts +227 -0
  7. package/src/activation-source.ts +93 -0
  8. package/src/blob-ref.ts +90 -0
  9. package/src/branded.ts +135 -0
  10. package/src/collections.ts +99 -0
  11. package/src/compose-batch.ts +69 -0
  12. package/src/compose-schema.ts +36 -0
  13. package/src/context.ts +66 -0
  14. package/src/derive.ts +485 -0
  15. package/src/detours.ts +8 -0
  16. package/src/diagnostics.ts +21 -0
  17. package/src/draft.ts +350 -0
  18. package/src/entity.ts +346 -0
  19. package/src/error-rendering.ts +87 -0
  20. package/src/errors.ts +483 -0
  21. package/src/execute.ts +1577 -0
  22. package/src/fetch.ts +138 -0
  23. package/src/fire.ts +1172 -0
  24. package/src/glob.ts +81 -0
  25. package/src/guards.ts +37 -0
  26. package/src/index.ts +704 -0
  27. package/src/internal/fork-ctx.ts +69 -0
  28. package/src/layer-field-rendering.ts +193 -0
  29. package/src/layer.ts +81 -0
  30. package/src/observe.ts +361 -0
  31. package/src/path-scope.ts +66 -0
  32. package/src/path-security.ts +98 -0
  33. package/src/patterns/bulk.ts +16 -0
  34. package/src/patterns/change.ts +12 -0
  35. package/src/patterns/date-range.ts +12 -0
  36. package/src/patterns/index.ts +8 -0
  37. package/src/patterns/pagination.ts +22 -0
  38. package/src/patterns/progress.ts +13 -0
  39. package/src/patterns/sorting.ts +14 -0
  40. package/src/patterns/status.ts +11 -0
  41. package/src/patterns/timestamps.ts +12 -0
  42. package/src/permits.ts +12 -0
  43. package/src/queue.ts +163 -0
  44. package/src/redaction/index.ts +3 -0
  45. package/src/redaction/patterns.ts +50 -0
  46. package/src/redaction/redactor.ts +178 -0
  47. package/src/resilience.ts +234 -0
  48. package/src/resource-config.ts +804 -0
  49. package/src/resource.ts +194 -0
  50. package/src/result.ts +212 -0
  51. package/src/run.ts +76 -0
  52. package/src/runtime-builtins.ts +69 -0
  53. package/src/schedule-runtime.ts +689 -0
  54. package/src/schedule.ts +326 -0
  55. package/src/serialization.ts +265 -0
  56. package/src/sha256.ts +136 -0
  57. package/src/signal-diagnostics.ts +633 -0
  58. package/src/signal-ref.ts +111 -0
  59. package/src/signal.ts +104 -0
  60. package/src/store/accessor-protocol.ts +56 -0
  61. package/src/store/index.ts +4 -0
  62. package/src/structured-examples.ts +248 -0
  63. package/src/surface-derivation.ts +91 -0
  64. package/src/surface-filter.ts +101 -0
  65. package/src/surface-overlay.ts +694 -0
  66. package/src/surface-versioning.ts +42 -0
  67. package/src/topo.ts +835 -0
  68. package/src/tracing.ts +346 -0
  69. package/src/trail-id-glob.ts +15 -0
  70. package/src/trail.ts +1351 -0
  71. package/src/trails/derive-trail.ts +835 -0
  72. package/src/trails/index.ts +9 -0
  73. package/src/trails/ingest.ts +152 -0
  74. package/src/trails-db.ts +212 -0
  75. package/src/transport-error-map.ts +163 -0
  76. package/src/type-utils.ts +87 -0
  77. package/src/types.ts +300 -0
  78. package/src/validate-established-topo.ts +73 -0
  79. package/src/validate-topo.ts +725 -0
  80. package/src/validation.ts +330 -0
  81. package/src/version-marker.ts +716 -0
  82. package/src/version-resolution.ts +308 -0
  83. package/src/version-runtime.ts +120 -0
  84. package/src/webhook.ts +461 -0
  85. package/src/workspace.ts +244 -0
  86. package/src/zod-wrappers.ts +72 -0
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+
3
+ import { matchesAnyGlob, matchesGlob } from './glob.js';
4
+
5
+ declare const pathGlobBrand: unique symbol;
6
+
7
+ export type PathGlob = string & {
8
+ readonly [pathGlobBrand]: 'PathGlob';
9
+ };
10
+
11
+ export const normalizePathScopePath = (value: string): string =>
12
+ value.replaceAll('\\', '/').replace(/^\.\//, '');
13
+
14
+ export const matchesPathGlob = (path: string, pattern: string): boolean =>
15
+ matchesGlob(normalizePathScopePath(path), normalizePathScopePath(pattern), {
16
+ separator: '/',
17
+ });
18
+
19
+ export const matchesAnyPathGlob = (
20
+ path: string,
21
+ patterns: readonly string[] | undefined
22
+ ): boolean =>
23
+ matchesAnyGlob(
24
+ normalizePathScopePath(path),
25
+ patterns?.map(normalizePathScopePath),
26
+ { separator: '/' }
27
+ );
28
+
29
+ const pathGlobArraySchema = z.array(z.string()).readonly();
30
+
31
+ export const pathScopeSchema = z
32
+ .object({
33
+ exclude: pathGlobArraySchema.optional(),
34
+ extensions: z.array(z.string()).readonly().optional(),
35
+ include: pathGlobArraySchema.optional(),
36
+ })
37
+ .strict();
38
+
39
+ export type PathScope = z.output<typeof pathScopeSchema>;
40
+
41
+ export type ScanTargets = Pick<PathScope, 'exclude' | 'extensions'>;
42
+
43
+ const extensionOf = (path: string): string => {
44
+ const normalized = normalizePathScopePath(path);
45
+ const name = normalized.slice(normalized.lastIndexOf('/') + 1);
46
+ const dot = name.lastIndexOf('.');
47
+ return dot <= 0 ? '' : name.slice(dot);
48
+ };
49
+
50
+ const normalizeExtension = (extension: string): string =>
51
+ extension === '' || extension.startsWith('.') ? extension : `.${extension}`;
52
+
53
+ const includedByExtension = (
54
+ path: string,
55
+ extensions: readonly string[] | undefined
56
+ ): boolean =>
57
+ extensions === undefined ||
58
+ extensions.length === 0 ||
59
+ extensions.map(normalizeExtension).includes(extensionOf(path));
60
+
61
+ export const includedByPathScope = (path: string, scope?: PathScope): boolean =>
62
+ (scope?.include === undefined ||
63
+ scope.include.length === 0 ||
64
+ matchesAnyPathGlob(path, scope.include)) &&
65
+ !matchesAnyPathGlob(path, scope?.exclude) &&
66
+ includedByExtension(path, scope?.extensions);
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Path security utilities for preventing path traversal attacks.
3
+ *
4
+ * All functions are runtime-agnostic (Node / Bun compatible).
5
+ */
6
+
7
+ import { PermissionError } from './errors.js';
8
+ import { Result } from './result.js';
9
+ // Path security guards filesystem access on tooling paths: node:path
10
+ // loads lazily at first use so the core barrel's module graph stays
11
+ // execution-portable on runtimes without node: builtins (TRL-1198).
12
+ import { loadRuntimeBuiltin } from './runtime-builtins.js';
13
+
14
+ const nodePath = () => loadRuntimeBuiltin('node:path');
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Internal
18
+ // ---------------------------------------------------------------------------
19
+
20
+ /** Returns true when `target` is equal to or a descendant of `base`. */
21
+ const isWithin = (base: string, target: string): boolean => {
22
+ const { isAbsolute, relative } = nodePath();
23
+ const rel = relative(base, target);
24
+ // Empty string means they are the same directory.
25
+ // A relative path starting with ".." means it escapes.
26
+ // An absolute path means a completely different tree.
27
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
28
+ };
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Public API
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Resolves `userPath` relative to `basePath` and ensures it stays within
36
+ * the base directory. Returns the resolved absolute path on success, or a
37
+ * `PermissionError` if the path escapes.
38
+ *
39
+ * Uses lexical path comparison (not `realpath`). Does not follow symlinks —
40
+ * if an attacker can create symlinks inside `basePath`, those could point
41
+ * outside the base. Use `realpath` before calling in symlink-sensitive environments.
42
+ */
43
+ export const securePath = (
44
+ basePath: string,
45
+ userPath: string
46
+ ): Result<string, PermissionError> => {
47
+ const { resolve } = nodePath();
48
+ const base = resolve(basePath);
49
+ const resolved = resolve(base, userPath);
50
+
51
+ if (!isWithin(base, resolved)) {
52
+ return Result.err(
53
+ new PermissionError(
54
+ `Path traversal detected: "${userPath}" escapes "${basePath}"`,
55
+ {
56
+ context: { basePath: base, resolved, userPath },
57
+ }
58
+ )
59
+ );
60
+ }
61
+
62
+ return Result.ok(resolved);
63
+ };
64
+
65
+ /**
66
+ * Returns `true` if `userPath` (resolved against `basePath`) stays within
67
+ * `basePath`.
68
+ */
69
+ export const isPathSafe = (basePath: string, userPath: string): boolean => {
70
+ const { resolve } = nodePath();
71
+ const base = resolve(basePath);
72
+ const resolved = resolve(base, userPath);
73
+ return isWithin(base, resolved);
74
+ };
75
+
76
+ /**
77
+ * Joins multiple path segments, resolves them against `basePath`, and
78
+ * validates the result stays within the base directory.
79
+ */
80
+ export const deriveSafePath = (
81
+ basePath: string,
82
+ ...segments: string[]
83
+ ): Result<string, PermissionError> => {
84
+ const { normalize, resolve } = nodePath();
85
+ const base = resolve(basePath);
86
+ const joined = resolve(base, ...segments.map((s) => normalize(s)));
87
+
88
+ if (!isWithin(base, joined)) {
89
+ return Result.err(
90
+ new PermissionError(
91
+ `Path traversal detected: segments escape "${basePath}"`,
92
+ { context: { basePath: base, resolved: joined, segments } }
93
+ )
94
+ );
95
+ }
96
+
97
+ return Result.ok(joined);
98
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Bulk operation schema helpers for @ontrails/core/patterns
3
+ */
4
+
5
+ import { z } from 'zod';
6
+
7
+ /** Bulk operation output wrapper for a given item schema. */
8
+ export const bulkOutput = <T>(itemSchema: z.ZodType<T>) =>
9
+ z.object({
10
+ errors: z
11
+ .array(z.object({ index: z.number(), message: z.string() }))
12
+ .optional(),
13
+ failed: z.number(),
14
+ items: z.array(itemSchema),
15
+ succeeded: z.number(),
16
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Change-tracking schema helpers for @ontrails/core/patterns
3
+ */
4
+
5
+ import { z } from 'zod';
6
+
7
+ /** Before/after change output for a given schema. */
8
+ export const changeOutput = <T>(schema: z.ZodType<T>) =>
9
+ z.object({
10
+ after: schema,
11
+ before: schema.optional(),
12
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Date-range schema helpers for @ontrails/core/patterns
3
+ */
4
+
5
+ import { z } from 'zod';
6
+
7
+ /** Optional since/until date-range fields. */
8
+ export const dateRangeFields = () =>
9
+ z.object({
10
+ since: z.string().optional(),
11
+ until: z.string().optional(),
12
+ });
@@ -0,0 +1,8 @@
1
+ export { paginationFields, paginatedOutput } from './pagination.js';
2
+ export { bulkOutput } from './bulk.js';
3
+ export { timestampFields } from './timestamps.js';
4
+ export { dateRangeFields } from './date-range.js';
5
+ export { sortFields } from './sorting.js';
6
+ export { statusFields } from './status.js';
7
+ export { changeOutput } from './change.js';
8
+ export { progressFields } from './progress.js';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Pagination schema helpers for @ontrails/core/patterns
3
+ */
4
+
5
+ import { z } from 'zod';
6
+
7
+ /** Common pagination input fields. */
8
+ export const paginationFields = () =>
9
+ z.object({
10
+ cursor: z.string().optional(),
11
+ limit: z.number().optional().default(20),
12
+ offset: z.number().optional().default(0),
13
+ });
14
+
15
+ /** Paginated output wrapper for a given item schema. */
16
+ export const paginatedOutput = <T>(itemSchema: z.ZodType<T>) =>
17
+ z.object({
18
+ hasMore: z.boolean(),
19
+ items: z.array(itemSchema),
20
+ nextCursor: z.string().optional(),
21
+ total: z.number(),
22
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Progress schema helpers for @ontrails/core/patterns
3
+ */
4
+
5
+ import { z } from 'zod';
6
+
7
+ /** Progress tracking fields. */
8
+ export const progressFields = () =>
9
+ z.object({
10
+ current: z.number(),
11
+ percentage: z.number().optional(),
12
+ total: z.number(),
13
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Sorting schema helpers for @ontrails/core/patterns
3
+ */
4
+
5
+ import { z } from 'zod';
6
+
7
+ /** Sort fields constrained to a set of allowed column names. */
8
+ export const sortFields = <const T extends string>(
9
+ allowedFields: [T, ...T[]]
10
+ ) =>
11
+ z.object({
12
+ sortBy: z.enum(allowedFields).optional(),
13
+ sortOrder: z.enum(['asc', 'desc']).optional().default('asc'),
14
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Status schema helpers for @ontrails/core/patterns
3
+ */
4
+
5
+ import { z } from 'zod';
6
+
7
+ /** Standard workflow status field. */
8
+ export const statusFields = () =>
9
+ z.object({
10
+ status: z.enum(['pending', 'running', 'completed', 'failed', 'cancelled']),
11
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Timestamp schema helpers for @ontrails/core/patterns
3
+ */
4
+
5
+ import { z } from 'zod';
6
+
7
+ /** Standard createdAt / updatedAt fields. */
8
+ export const timestampFields = () =>
9
+ z.object({
10
+ createdAt: z.string(),
11
+ updatedAt: z.string(),
12
+ });
package/src/permits.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+
3
+ /** Minimal permit shape available on TrailContext. */
4
+ export interface BasePermit {
5
+ readonly id: string;
6
+ readonly scopes: readonly string[];
7
+ }
8
+
9
+ export const basePermitSchema: z.ZodType<BasePermit> = z.object({
10
+ id: z.string(),
11
+ scopes: z.array(z.string()).readonly(),
12
+ });
package/src/queue.ts ADDED
@@ -0,0 +1,163 @@
1
+ import type {
2
+ ActivationSource,
3
+ ActivationSourceMeta,
4
+ ActivationSourceParse,
5
+ } from './activation-source.js';
6
+ import { ValidationError } from './errors.js';
7
+
8
+ export interface QueueSpec<TOutput = unknown> {
9
+ readonly meta?: ActivationSourceMeta | undefined;
10
+ readonly parse: ActivationSourceParse<TOutput>;
11
+ readonly payload?: ActivationSource['payload'] | undefined;
12
+ /**
13
+ * Runtime queue name. Defaults to the source id, so authored queue
14
+ * contracts can stay stable while host bindings choose their own names.
15
+ */
16
+ readonly queue?: string | undefined;
17
+ /** Reserved for future queue-specific design; trail versioning is trail-only. */
18
+ readonly version?: never;
19
+ }
20
+
21
+ export interface QueueSource<TOutput = unknown> extends ActivationSource {
22
+ readonly kind: 'queue';
23
+ readonly meta?: ActivationSourceMeta | undefined;
24
+ readonly parse: ActivationSourceParse<TOutput>;
25
+ readonly payload?: ActivationSource['payload'] | undefined;
26
+ readonly queue: string;
27
+ }
28
+
29
+ export interface QueueValidationIssue {
30
+ readonly field: 'parse' | 'queue';
31
+ readonly message: string;
32
+ }
33
+
34
+ const normalizeQueueName = (queueName: string): string => queueName.trim();
35
+
36
+ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
37
+ typeof value === 'object' && value !== null && !Array.isArray(value);
38
+
39
+ const isZodSchema = (value: unknown): boolean =>
40
+ isObjectRecord(value) && typeof value['safeParse'] === 'function';
41
+
42
+ const validateQueueName = (queueName: unknown): QueueValidationIssue[] =>
43
+ typeof queueName === 'string' && queueName.trim().length > 0
44
+ ? []
45
+ : [
46
+ {
47
+ field: 'queue',
48
+ message: 'Queue source must define a non-empty queue name',
49
+ },
50
+ ];
51
+
52
+ const validateRequiredParse = (parse: unknown): QueueValidationIssue[] =>
53
+ parse === undefined
54
+ ? [
55
+ {
56
+ field: 'parse',
57
+ message: 'Queue sources must define parse',
58
+ },
59
+ ]
60
+ : [];
61
+
62
+ const validateParseShape = (parse: unknown): QueueValidationIssue[] => {
63
+ if (parse === undefined || isZodSchema(parse)) {
64
+ return [];
65
+ }
66
+ if (isObjectRecord(parse) && isZodSchema(parse['output'])) {
67
+ return [];
68
+ }
69
+ return [
70
+ {
71
+ field: 'parse',
72
+ message: 'Queue parse must be a Zod schema or define parse.output',
73
+ },
74
+ ];
75
+ };
76
+
77
+ const queueIssuesMessage = (
78
+ id: string,
79
+ issues: readonly QueueValidationIssue[]
80
+ ): string =>
81
+ `queue("${id}") is invalid: ${issues.map((issue) => `${issue.field}: ${issue.message}`).join('; ')}`;
82
+
83
+ const assertQueueSpec = <TOutput>(
84
+ id: string,
85
+ spec: QueueSpec<TOutput>
86
+ ): {
87
+ readonly queue: string;
88
+ } => {
89
+ const queueName = spec.queue === undefined ? id : spec.queue;
90
+ const issues = [
91
+ ...validateQueueName(queueName),
92
+ ...validateRequiredParse(spec.parse),
93
+ ...validateParseShape(spec.parse),
94
+ ];
95
+
96
+ if (issues.length > 0) {
97
+ throw new ValidationError(queueIssuesMessage(id, issues), {
98
+ context: { issues },
99
+ });
100
+ }
101
+
102
+ return { queue: normalizeQueueName(queueName) };
103
+ };
104
+
105
+ export const validateQueueSource = (
106
+ source: ActivationSource
107
+ ): readonly QueueValidationIssue[] => {
108
+ if (source.kind !== 'queue') {
109
+ return [];
110
+ }
111
+
112
+ return [
113
+ ...validateQueueName(source.queue),
114
+ ...validateRequiredParse(source.parse),
115
+ ...validateParseShape(source.parse),
116
+ ];
117
+ };
118
+
119
+ /**
120
+ * Define a queue activation source.
121
+ *
122
+ * Queue sources are inert contract data: they describe which runtime queue
123
+ * wakes a trail and how the message body becomes trail input. A host adapter
124
+ * such as `@ontrails/cloudflare/workers` owns delivery.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * import { queue } from '@ontrails/core';
129
+ * import { z } from 'zod';
130
+ *
131
+ * const source = queue('queue.email.outbox', {
132
+ * queue: 'email-outbox',
133
+ * parse: z.object({ messageId: z.string() }),
134
+ * });
135
+ * ```
136
+ */
137
+ export function queue<TOutput>(
138
+ id: string,
139
+ spec: QueueSpec<TOutput>
140
+ ): QueueSource<TOutput>;
141
+ export function queue<TOutput>(
142
+ spec: QueueSpec<TOutput> & { readonly id: string }
143
+ ): QueueSource<TOutput>;
144
+ export function queue<TOutput>(
145
+ idOrSpec: string | (QueueSpec<TOutput> & { readonly id: string }),
146
+ maybeSpec?: QueueSpec<TOutput>
147
+ ): QueueSource<TOutput> {
148
+ const id = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
149
+ // oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
150
+ const spec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
151
+ const normalized = assertQueueSpec(id, spec);
152
+
153
+ return Object.freeze({
154
+ id,
155
+ kind: 'queue' as const,
156
+ parse: spec.parse,
157
+ queue: normalized.queue,
158
+ ...(spec.meta === undefined
159
+ ? {}
160
+ : { meta: Object.freeze({ ...spec.meta }) }),
161
+ ...(spec.payload === undefined ? {} : { payload: spec.payload }),
162
+ });
163
+ }
@@ -0,0 +1,3 @@
1
+ export { DEFAULT_PATTERNS, DEFAULT_SENSITIVE_KEYS } from './patterns.js';
2
+ export { createRedactor } from './redactor.js';
3
+ export type { Redactor, RedactorConfig } from './redactor.js';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Default redaction patterns and sensitive key lists.
3
+ *
4
+ * These are used by {@link createRedactor} to strip secrets from strings
5
+ * and object values before they reach logs, traces, or error payloads.
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Regex patterns that match sensitive values inside arbitrary strings
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export const DEFAULT_PATTERNS: RegExp[] = [
13
+ // Credit card numbers: 4 groups of 4 digits separated by spaces or dashes
14
+ /\b\d{4}[- ]\d{4}[- ]\d{4}[- ]\d{4}\b/g,
15
+
16
+ // SSN: XXX-XX-XXXX
17
+ /\b\d{3}-\d{2}-\d{4}\b/g,
18
+
19
+ // Bearer tokens. Keep the threshold above short prose like "Bearer token".
20
+ /Bearer\s+[A-Za-z0-9\-._~+/=]{6,}/g,
21
+
22
+ // Basic auth
23
+ /Basic\s+[A-Za-z0-9+/=]{8,}/g,
24
+
25
+ // Common key/value secrets embedded in strings
26
+ /\b(?:password|secret|token|api[_-]?key|cookie)\s*[:=]\s*[^\s,;]+/gi,
27
+
28
+ // API keys: sk-*, pk_*, sk_* prefixed tokens
29
+ /\b(?:sk-|pk_|sk_)[A-Za-z0-9_-]{8,}\b/g,
30
+
31
+ // JWT tokens: eyJ... (three base64url segments separated by dots)
32
+ /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
33
+ ];
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Object keys whose values should always be redacted
37
+ // ---------------------------------------------------------------------------
38
+
39
+ export const DEFAULT_SENSITIVE_KEYS: string[] = [
40
+ 'password',
41
+ 'secret',
42
+ 'token',
43
+ 'apiKey',
44
+ 'api_key',
45
+ 'authorization',
46
+ 'cookie',
47
+ 'ssn',
48
+ 'creditCard',
49
+ 'credit_card',
50
+ ];