@potentiajs/core 0.1.0-preview.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 (56) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +441 -0
  4. package/bin/potentia.js +14 -0
  5. package/examples/file-routing-basic/README.md +156 -0
  6. package/examples/file-routing-basic/app.js +16 -0
  7. package/examples/file-routing-basic/generate.js +21 -0
  8. package/examples/file-routing-basic/routes/health.js +3 -0
  9. package/examples/file-routing-basic/routes/index.js +3 -0
  10. package/examples/file-routing-basic/routes/users/[id].js +6 -0
  11. package/examples/file-routing-basic/routes/users/_routes.js +15 -0
  12. package/examples/form-rendering-basic/README.md +43 -0
  13. package/examples/form-rendering-basic/index.js +120 -0
  14. package/examples/full-flow-basic/README.md +141 -0
  15. package/examples/full-flow-basic/app.js +16 -0
  16. package/examples/full-flow-basic/form.js +205 -0
  17. package/examples/full-flow-basic/generate.js +21 -0
  18. package/examples/full-flow-basic/routes/index.js +5 -0
  19. package/examples/full-flow-basic/routes/users/[id].js +5 -0
  20. package/examples/full-flow-basic/routes/users/_routes.js +8 -0
  21. package/examples/full-flow-basic/routes/users/index.js +5 -0
  22. package/examples/full-flow-basic/routes/users/new.js +5 -0
  23. package/package.json +90 -0
  24. package/src/cli.js +407 -0
  25. package/src/dev/file-routing/diagnostics.js +24 -0
  26. package/src/dev/file-routing/generator.js +126 -0
  27. package/src/dev/file-routing/index.js +5 -0
  28. package/src/dev/file-routing/path-mapping.js +125 -0
  29. package/src/dev/file-routing/scanner.js +154 -0
  30. package/src/dev/file-routing/writer.js +100 -0
  31. package/src/file-routing.d.ts +36 -0
  32. package/src/file-routing.js +1 -0
  33. package/src/forms.d.ts +9 -0
  34. package/src/forms.js +334 -0
  35. package/src/index.d.ts +185 -0
  36. package/src/index.js +15 -0
  37. package/src/index.mjs +25 -0
  38. package/src/kernel/action-projection.js +35 -0
  39. package/src/kernel/action.js +172 -0
  40. package/src/kernel/app.js +144 -0
  41. package/src/kernel/context.js +37 -0
  42. package/src/kernel/contract-projection.js +129 -0
  43. package/src/kernel/contract.js +135 -0
  44. package/src/kernel/diagnostics.js +147 -0
  45. package/src/kernel/effect.js +119 -0
  46. package/src/kernel/error.js +93 -0
  47. package/src/kernel/form-projection.js +251 -0
  48. package/src/kernel/form-state.js +173 -0
  49. package/src/kernel/plugin.js +46 -0
  50. package/src/kernel/response.js +187 -0
  51. package/src/kernel/result.js +45 -0
  52. package/src/kernel/route-collection.js +148 -0
  53. package/src/kernel/route-id.js +11 -0
  54. package/src/kernel/route-manifest.js +129 -0
  55. package/src/kernel/route-projection.js +139 -0
  56. package/src/kernel/route.js +156 -0
@@ -0,0 +1,147 @@
1
+ const KNOWN_SOURCES = new Set(['sigil', 'generic', 'framework', 'unknown']);
2
+ const KNOWN_BOUNDARIES = new Set(['params', 'query', 'headers', 'body', 'response', 'input', 'output', 'handler', 'contract']);
3
+ const SAFE_TYPE_DESCRIPTORS = new Set(['string', 'number', 'boolean', 'null', 'undefined', 'array', 'object', 'function', 'symbol', 'bigint']);
4
+
5
+ export function normalizeIssue(input = null, options = {}) {
6
+ if (!input || typeof input !== 'object') {
7
+ return createRootIssue(options);
8
+ }
9
+
10
+ const path = normalizePath(input.path);
11
+
12
+ return {
13
+ code: safeString(input.code) || safeString(options.code) || 'contract_failed',
14
+ message: safeString(input.message) || safeString(options.message) || 'Contract rejected value',
15
+ path: path,
16
+ field: deriveField(path),
17
+ boundary: normalizeBoundary(input.boundary, options.boundary),
18
+ source: normalizeSource(input.source, options.source),
19
+ expected: safeExpected(Object.prototype.hasOwnProperty.call(input, 'expected') ? input.expected : options.expected),
20
+ received: safeReceived(Object.prototype.hasOwnProperty.call(input, 'received') ? input.received : options.received),
21
+ meta: null
22
+ };
23
+ }
24
+
25
+ export function normalizeIssues(input = null, options = {}) {
26
+ const rawIssues = Array.isArray(input) && input.length > 0 ? input : [input];
27
+ return rawIssues.map((issue) => normalizeIssue(issue, options)).sort(compareIssues);
28
+ }
29
+
30
+ export function createRootIssue(options = {}) {
31
+ return {
32
+ code: safeString(options.code) || 'contract_failed',
33
+ message: safeString(options.message) || 'Contract rejected value',
34
+ path: [],
35
+ field: null,
36
+ boundary: normalizeBoundary(options.boundary),
37
+ source: normalizeSource(options.source),
38
+ expected: safeExpected(options.expected),
39
+ received: safeReceived(options.received),
40
+ meta: null
41
+ };
42
+ }
43
+
44
+ export function deriveField(path) {
45
+ const normalized = normalizePath(path);
46
+ if (normalized.length === 0) return null;
47
+
48
+ let field = '';
49
+ for (const segment of normalized) {
50
+ if (typeof segment === 'number') {
51
+ field += `[${segment}]`;
52
+ continue;
53
+ }
54
+
55
+ field += field.length === 0 ? segment : `.${segment}`;
56
+ }
57
+
58
+ return field || null;
59
+ }
60
+
61
+ export function normalizePath(path) {
62
+ if (!Array.isArray(path)) return [];
63
+
64
+ return path.filter((segment) => {
65
+ if (typeof segment === 'string') return segment.length > 0;
66
+ if (typeof segment === 'number') return Number.isInteger(segment) && segment >= 0;
67
+ return false;
68
+ });
69
+ }
70
+
71
+ export function safeReceived(value) {
72
+ if (value === undefined) return null;
73
+ if (value === null) return 'null';
74
+
75
+ if (typeof value === 'string') {
76
+ return SAFE_TYPE_DESCRIPTORS.has(value) ? value : primitiveType(value);
77
+ }
78
+
79
+ return primitiveType(value);
80
+ }
81
+
82
+ export function safeExpected(value) {
83
+ if (value === undefined || value === null) return null;
84
+ if (typeof value === 'string') return value.length > 0 && value.length <= 80 ? value : null;
85
+ if (typeof value === 'function' && typeof value.name === 'string' && value.name.length > 0) return value.name.toLowerCase();
86
+ return SAFE_TYPE_DESCRIPTORS.has(primitiveType(value)) ? primitiveType(value) : null;
87
+ }
88
+
89
+ export function sigilIssueFromError(error, boundary) {
90
+ if (!error || typeof error !== 'object' || error.code !== 'SIGIL_VALIDATION_FAILED') {
91
+ return createRootIssue({ boundary: boundary, source: 'sigil' });
92
+ }
93
+
94
+ const received = Object.prototype.hasOwnProperty.call(error, 'actual') ? safeReceived(error.actual) : null;
95
+ const expected = safeExpected(error.expected);
96
+ const code = expected && received === 'undefined' ? 'missing_required' : expected && received ? 'invalid_type' : 'contract_failed';
97
+
98
+ return normalizeIssue({
99
+ code: code,
100
+ message: 'SigilJS contract rejected value',
101
+ path: error.path,
102
+ boundary: boundary,
103
+ source: 'sigil',
104
+ expected: expected,
105
+ ...(received === null ? {} : { received: received })
106
+ }, { boundary: boundary, source: 'sigil' });
107
+ }
108
+
109
+ function normalizeBoundary(value, fallback = null) {
110
+ const boundary = safeString(value) || safeString(fallback) || 'contract';
111
+ return KNOWN_BOUNDARIES.has(boundary) ? boundary : 'contract';
112
+ }
113
+
114
+ function normalizeSource(value, fallback = null) {
115
+ const source = safeString(value) || safeString(fallback) || 'generic';
116
+ return KNOWN_SOURCES.has(source) ? source : 'unknown';
117
+ }
118
+
119
+ function safeString(value) {
120
+ return typeof value === 'string' && value.length > 0 ? value : null;
121
+ }
122
+
123
+ function primitiveType(value) {
124
+ if (value === null) return 'null';
125
+ if (Array.isArray(value)) return 'array';
126
+ return typeof value;
127
+ }
128
+
129
+ function compareIssues(left, right) {
130
+ const path = comparePaths(left.path, right.path);
131
+ if (path !== 0) return path;
132
+ const code = left.code.localeCompare(right.code);
133
+ if (code !== 0) return code;
134
+ return left.message.localeCompare(right.message);
135
+ }
136
+
137
+ function comparePaths(left, right) {
138
+ if (left.length !== right.length) return left.length - right.length;
139
+ for (let index = 0; index < left.length; index += 1) {
140
+ const a = left[index];
141
+ const b = right[index];
142
+ if (typeof a === 'number' && typeof b === 'number' && a !== b) return a - b;
143
+ const compared = String(a).localeCompare(String(b));
144
+ if (compared !== 0) return compared;
145
+ }
146
+ return 0;
147
+ }
@@ -0,0 +1,119 @@
1
+ export function effect(generatorOrFunction) {
2
+ return {
3
+ type: 'effect',
4
+ run: generatorOrFunction
5
+ };
6
+ }
7
+
8
+ export function call(fn, ...args) {
9
+ return {
10
+ type: 'call',
11
+ fn: fn,
12
+ args: args,
13
+ meta: null
14
+ };
15
+ }
16
+
17
+ export function value(value) {
18
+ return {
19
+ type: 'value',
20
+ value: value,
21
+ meta: null
22
+ };
23
+ }
24
+
25
+ export function context(key) {
26
+ return {
27
+ type: 'context',
28
+ key: key,
29
+ meta: null
30
+ };
31
+ }
32
+
33
+ export async function runEffect(effectValue, context = null, ...args) {
34
+ if (isEffectDescriptor(effectValue)) {
35
+ return runEffectFunction(effectValue.run, context, args);
36
+ }
37
+
38
+ if (typeof effectValue === 'function') {
39
+ return runEffectFunction(effectValue, context, args);
40
+ }
41
+
42
+ return effectValue;
43
+ }
44
+
45
+ async function runEffectFunction(fn, context, args) {
46
+ const value = fn(context, ...args);
47
+
48
+ if (isIterator(value)) {
49
+ return runIterator(value, context);
50
+ }
51
+
52
+ return await value;
53
+ }
54
+
55
+ async function runIterator(iterator, context) {
56
+ let input = undefined;
57
+
58
+ while (true) {
59
+ const next = iterator.next(input);
60
+ if (next.done) return await next.value;
61
+ input = await runCommand(next.value, context);
62
+ }
63
+ }
64
+
65
+ async function runCommand(command, context) {
66
+ if (!command || typeof command !== 'object') return command;
67
+
68
+ validateCommand(command);
69
+
70
+ if (command.type === 'value') return command.value;
71
+
72
+ if (command.type === 'call') {
73
+ const args = Object.prototype.hasOwnProperty.call(command, 'args') ? command.args : [];
74
+ return await command.fn(...args);
75
+ }
76
+
77
+ if (command.type === 'context') {
78
+ return context ? context[command.key] : undefined;
79
+ }
80
+
81
+ throw new Error(`Unsupported effect command type: ${command.type}`);
82
+ }
83
+
84
+ function validateCommand(command) {
85
+ if (!Object.prototype.hasOwnProperty.call(command, 'type')) {
86
+ throw new Error('Invalid effect command: type must be a string');
87
+ }
88
+
89
+ if (typeof command.type !== 'string' || command.type.length === 0) {
90
+ throw new Error('Invalid effect command: type must be a string');
91
+ }
92
+
93
+ if (command.type === 'value') return;
94
+
95
+ if (command.type === 'call') {
96
+ if (typeof command.fn !== 'function') {
97
+ throw new Error('Invalid effect call command: fn must be a function');
98
+ }
99
+ if (Object.prototype.hasOwnProperty.call(command, 'args') && !Array.isArray(command.args)) {
100
+ throw new Error('Invalid effect call command: args must be an array when provided');
101
+ }
102
+ return;
103
+ }
104
+
105
+ if (command.type === 'context') {
106
+ if (typeof command.key !== 'string' || command.key.length === 0) {
107
+ throw new Error('Invalid effect context command: key must be a non-empty string');
108
+ }
109
+ return;
110
+ }
111
+ }
112
+
113
+ function isEffectDescriptor(value) {
114
+ return Boolean(value && typeof value === 'object' && value.type === 'effect' && typeof value.run === 'function');
115
+ }
116
+
117
+ function isIterator(value) {
118
+ return Boolean(value && typeof value === 'object' && typeof value.next === 'function');
119
+ }
@@ -0,0 +1,93 @@
1
+ export const ERROR_CODES = {
2
+ NOT_FOUND: 'POTENTIA_NOT_FOUND',
3
+ METHOD_NOT_ALLOWED: 'POTENTIA_METHOD_NOT_ALLOWED',
4
+ CONTRACT_FAILED: 'POTENTIA_CONTRACT_FAILED',
5
+ RESPONSE_CONTRACT_FAILED: 'POTENTIA_RESPONSE_CONTRACT_FAILED',
6
+ ACTION_INPUT_FAILED: 'POTENTIA_ACTION_INPUT_FAILED',
7
+ ACTION_OUTPUT_FAILED: 'POTENTIA_ACTION_OUTPUT_FAILED',
8
+ ACTION_HANDLER_FAILED: 'POTENTIA_ACTION_HANDLER_FAILED',
9
+ HANDLER_FAILED: 'POTENTIA_HANDLER_FAILED',
10
+ BAD_REQUEST: 'POTENTIA_BAD_REQUEST'
11
+ };
12
+
13
+ export function createFrameworkError(code, message, options = {}) {
14
+ return {
15
+ name: 'PotentiaError',
16
+ code: code,
17
+ message: message,
18
+ status: Number.isInteger(options.status) ? options.status : statusForCode(code),
19
+ detail: Object.prototype.hasOwnProperty.call(options, 'detail') ? options.detail : null,
20
+ cause: Object.prototype.hasOwnProperty.call(options, 'cause') ? options.cause : null,
21
+ expose: Object.prototype.hasOwnProperty.call(options, 'expose') ? Boolean(options.expose) : defaultExpose(code)
22
+ };
23
+ }
24
+
25
+ export function normalizeFrameworkError(error) {
26
+ if (isFrameworkError(error)) {
27
+ return createFrameworkError(error.code, visibleMessage(error), {
28
+ status: error.status,
29
+ detail: error.detail,
30
+ cause: error.cause,
31
+ expose: error.expose
32
+ });
33
+ }
34
+
35
+ if (error && typeof error === 'object' && typeof error.code === 'string') {
36
+ const mapped = mapLegacyCode(error.code);
37
+ return createFrameworkError(mapped, error.message || defaultMessage(mapped), {
38
+ status: Number.isInteger(error.status) ? error.status : statusForCode(mapped),
39
+ detail: error.detail ?? error.details ?? null,
40
+ cause: error.cause ?? null,
41
+ expose: error.expose ?? defaultExpose(mapped)
42
+ });
43
+ }
44
+
45
+ return createFrameworkError(ERROR_CODES.HANDLER_FAILED, 'Internal server error', {
46
+ status: 500,
47
+ detail: null,
48
+ cause: error,
49
+ expose: false
50
+ });
51
+ }
52
+
53
+ export function isFrameworkError(error) {
54
+ return Boolean(error && typeof error === 'object' && error.name === 'PotentiaError' && typeof error.code === 'string');
55
+ }
56
+
57
+ function visibleMessage(error) {
58
+ return error.expose ? error.message : defaultMessage(error.code);
59
+ }
60
+
61
+ function mapLegacyCode(code) {
62
+ if (code === 'NOT_FOUND') return ERROR_CODES.NOT_FOUND;
63
+ if (code === 'METHOD_NOT_ALLOWED') return ERROR_CODES.METHOD_NOT_ALLOWED;
64
+ if (code === 'BAD_REQUEST') return ERROR_CODES.CONTRACT_FAILED;
65
+ if (code === 'RESPONSE_CONTRACT_FAILED') return ERROR_CODES.RESPONSE_CONTRACT_FAILED;
66
+ if (code === 'ERROR') return ERROR_CODES.HANDLER_FAILED;
67
+ return code;
68
+ }
69
+
70
+ function statusForCode(code) {
71
+ if (code === ERROR_CODES.NOT_FOUND) return 404;
72
+ if (code === ERROR_CODES.METHOD_NOT_ALLOWED) return 405;
73
+ if (code === ERROR_CODES.CONTRACT_FAILED) return 400;
74
+ if (code === ERROR_CODES.ACTION_INPUT_FAILED) return 400;
75
+ if (code === ERROR_CODES.BAD_REQUEST) return 400;
76
+ return 500;
77
+ }
78
+
79
+ function defaultExpose(code) {
80
+ return code !== ERROR_CODES.HANDLER_FAILED;
81
+ }
82
+
83
+ function defaultMessage(code) {
84
+ if (code === ERROR_CODES.NOT_FOUND) return 'No route matched the request path';
85
+ if (code === ERROR_CODES.METHOD_NOT_ALLOWED) return 'Route matched the path, but not the request method';
86
+ if (code === ERROR_CODES.CONTRACT_FAILED) return 'Request failed contract validation';
87
+ if (code === ERROR_CODES.RESPONSE_CONTRACT_FAILED) return 'Response failed contract validation';
88
+ if (code === ERROR_CODES.ACTION_INPUT_FAILED) return 'Action input contract failed.';
89
+ if (code === ERROR_CODES.ACTION_OUTPUT_FAILED) return 'Action output contract failed.';
90
+ if (code === ERROR_CODES.ACTION_HANDLER_FAILED) return 'Internal server error';
91
+ if (code === ERROR_CODES.BAD_REQUEST) return 'Bad request';
92
+ return 'Internal server error';
93
+ }
@@ -0,0 +1,251 @@
1
+ import { isAction } from './action.js';
2
+ import { projectContract } from './contract-projection.js';
3
+
4
+ const ROOT_ERROR_KEY = '_form';
5
+ const DEFAULT_METHOD = 'POST';
6
+ const DEFAULT_ENCTYPE = 'application/x-www-form-urlencoded';
7
+ const SENSITIVE_SEGMENTS = new Set([
8
+ 'password',
9
+ 'confirmpassword',
10
+ 'currentpassword',
11
+ 'newpassword',
12
+ 'token',
13
+ 'secret',
14
+ 'apikey',
15
+ 'authorization',
16
+ 'auth',
17
+ 'credential',
18
+ 'session',
19
+ 'cookie'
20
+ ]);
21
+
22
+ export function projectForm(value, options = {}) {
23
+ if (!isAction(value)) {
24
+ return createFormProjection({
25
+ id: optionString(options.id, null),
26
+ actionId: null,
27
+ method: optionString(options.method, DEFAULT_METHOD),
28
+ encType: optionString(options.encType, DEFAULT_ENCTYPE),
29
+ opaque: true,
30
+ fields: null,
31
+ reason: 'Input is not an action',
32
+ meta: null
33
+ });
34
+ }
35
+
36
+ const contractProjection = value.input ? projectContract(value.input) : null;
37
+ const fieldResult = projectFieldsFromContractProjection(contractProjection);
38
+
39
+ return createFormProjection({
40
+ id: optionString(options.id, value.id),
41
+ actionId: value.id,
42
+ method: optionString(options.method, DEFAULT_METHOD),
43
+ encType: optionString(options.encType, DEFAULT_ENCTYPE),
44
+ opaque: fieldResult.opaque,
45
+ fields: fieldResult.fields,
46
+ reason: fieldResult.reason,
47
+ meta: value.meta || null
48
+ });
49
+ }
50
+
51
+ export function createFieldProjection(options = {}) {
52
+ const path = Array.isArray(options.path) ? options.path.slice() : fieldPath(options.name || options.field || '');
53
+ const field = typeof options.field === 'string' && options.field.length > 0 ? options.field : pathToField(path);
54
+ const kind = typeof options.kind === 'string' && options.kind.length > 0 ? options.kind : 'unknown';
55
+ const multiple = Object.prototype.hasOwnProperty.call(options, 'multiple') ? Boolean(options.multiple) : kind === 'array';
56
+ const sensitive = isSensitiveField(path.length > 0 ? path : field);
57
+ const input = deriveInputHint({ field: field, path: path, kind: kind, multiple: multiple, sensitive: sensitive });
58
+
59
+ return {
60
+ kind: 'field',
61
+ name: typeof options.name === 'string' && options.name.length > 0 ? options.name : field,
62
+ path: path,
63
+ field: field,
64
+ label: typeof options.label === 'string' && options.label.length > 0 ? options.label : deriveLabel(field),
65
+ help: null,
66
+ placeholder: null,
67
+ input: input,
68
+ required: Object.prototype.hasOwnProperty.call(options, 'required') ? Boolean(options.required) : null,
69
+ multiple: multiple,
70
+ sensitive: sensitive,
71
+ options: Array.isArray(options.options) ? options.options : null,
72
+ defaultValue: null,
73
+ contract: {
74
+ kind: typeof options.contractKind === 'string' && options.contractKind.length > 0 ? options.contractKind : 'unknown',
75
+ expected: kind
76
+ },
77
+ meta: null
78
+ };
79
+ }
80
+
81
+ export function deriveLabel(field) {
82
+ if (typeof field !== 'string' || field.length === 0) return '';
83
+ const leaf = field.replace(/\[[0-9]+\]/g, '').split('.').filter(Boolean).join(' ');
84
+ return leaf
85
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
86
+ .replace(/[_-]+/g, ' ')
87
+ .split(' ')
88
+ .filter(Boolean)
89
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
90
+ .join(' ');
91
+ }
92
+
93
+ export function deriveInputHint(field, contractSummary = {}) {
94
+ const normalized = normalizeFieldName(field && field.field ? field.field : '');
95
+ const kind = field && typeof field.kind === 'string' ? field.kind : contractSummary.expected;
96
+ const sensitive = Boolean(field && field.sensitive);
97
+ let type = primitiveInputType(kind);
98
+ let autocomplete = null;
99
+
100
+ if (normalized === 'email') {
101
+ type = 'email';
102
+ autocomplete = 'email';
103
+ } else if (normalized === 'url' || normalized === 'website') {
104
+ type = 'url';
105
+ } else if (normalized === 'phone' || normalized === 'tel') {
106
+ type = 'tel';
107
+ autocomplete = 'tel';
108
+ } else if (isPasswordLike(normalized)) {
109
+ type = 'password';
110
+ autocomplete = normalized === 'currentpassword' ? 'current-password' : null;
111
+ } else if (normalized === 'name') {
112
+ autocomplete = 'name';
113
+ } else if (normalized === 'username') {
114
+ autocomplete = 'username';
115
+ }
116
+
117
+ if (sensitive && isPasswordLike(normalized)) type = 'password';
118
+
119
+ return {
120
+ type: type,
121
+ mode: null,
122
+ autocomplete: autocomplete
123
+ };
124
+ }
125
+
126
+ export function isSensitiveField(pathOrField) {
127
+ const segments = Array.isArray(pathOrField) ? pathOrField : fieldPath(pathOrField);
128
+ return segments.some((segment) => typeof segment === 'string' && isSensitiveSegment(segment));
129
+ }
130
+
131
+ export function projectFieldsFromContractProjection(projection) {
132
+ if (!projection) return opaqueFields('Action input contract is missing');
133
+ if (projection.opaque) return opaqueFields('Action input contract is opaque');
134
+ if (projection.kind !== 'sigil') return opaqueFields('Action input contract is unsupported');
135
+ if (!Array.isArray(projection.fields)) return opaqueFields('Action input contract fields are unavailable');
136
+
137
+ const fields = flattenProjectedFields(projection.fields, [], projection.kind);
138
+ if (fields.length === 0) return opaqueFields('Action input contract fields are unavailable');
139
+
140
+ return { opaque: false, fields: fields, reason: null };
141
+ }
142
+
143
+ function flattenProjectedFields(fields, parentPath, contractKind) {
144
+ const result = [];
145
+ const ordered = Array.isArray(fields) ? fields.slice() : [];
146
+
147
+ for (const field of ordered) {
148
+ if (!field || typeof field.name !== 'string') continue;
149
+ const path = parentPath.concat(field.name);
150
+ const kind = typeof field.kind === 'string' ? field.kind : 'unknown';
151
+
152
+ if (kind === 'object' && Array.isArray(field.fields) && field.fields.length > 0) {
153
+ result.push(...flattenProjectedFields(field.fields, path, contractKind));
154
+ continue;
155
+ }
156
+
157
+ result.push(createFieldProjection({
158
+ name: pathToField(path),
159
+ path: path,
160
+ field: pathToField(path),
161
+ kind: kind,
162
+ required: field.required === true,
163
+ multiple: kind === 'array',
164
+ contractKind: contractKind
165
+ }));
166
+ }
167
+
168
+ return result;
169
+ }
170
+
171
+ function createFormProjection(input) {
172
+ return {
173
+ kind: 'form',
174
+ id: input.id,
175
+ actionId: input.actionId,
176
+ method: input.method,
177
+ encType: input.encType,
178
+ opaque: input.opaque,
179
+ fields: input.fields,
180
+ reason: input.reason,
181
+ errors: { rootKey: ROOT_ERROR_KEY },
182
+ values: { preservation: 'safe-parsed-values' },
183
+ validation: {
184
+ server: 'authoritative',
185
+ client: 'projection-only'
186
+ },
187
+ redirect: { afterPost: 'explicit-303-recommended' },
188
+ meta: input.meta
189
+ };
190
+ }
191
+
192
+ function opaqueFields(reason) {
193
+ return { opaque: true, fields: null, reason: reason };
194
+ }
195
+
196
+ function optionString(value, fallback) {
197
+ return typeof value === 'string' && value.length > 0 ? value : fallback;
198
+ }
199
+
200
+ function primitiveInputType(kind) {
201
+ if (kind === 'number' || kind === 'integer') return 'number';
202
+ if (kind === 'boolean') return 'checkbox';
203
+ return 'text';
204
+ }
205
+
206
+ function isPasswordLike(normalized) {
207
+ return normalized === 'password' || normalized === 'confirmpassword' || normalized === 'currentpassword' || normalized === 'newpassword';
208
+ }
209
+
210
+ function isSensitiveSegment(segment) {
211
+ const normalized = normalizeFieldName(segment);
212
+ if (SENSITIVE_SEGMENTS.has(normalized)) return true;
213
+ for (const sensitive of SENSITIVE_SEGMENTS) {
214
+ if (normalized.includes(sensitive)) return true;
215
+ }
216
+ return false;
217
+ }
218
+
219
+ function fieldPath(field) {
220
+ if (typeof field !== 'string' || field.length === 0) return [];
221
+ const path = [];
222
+ for (const part of field.split('.')) {
223
+ const match = /^([^\[]+)(?:\[([0-9]+)\])?$/.exec(part);
224
+ if (!match) {
225
+ path.push(part);
226
+ continue;
227
+ }
228
+ path.push(match[1]);
229
+ if (match[2] !== undefined) path.push(Number(match[2]));
230
+ }
231
+ return path;
232
+ }
233
+
234
+ function pathToField(path) {
235
+ if (!Array.isArray(path) || path.length === 0) return '';
236
+ let field = '';
237
+ for (const segment of path) {
238
+ if (typeof segment === 'number') {
239
+ field += `[${segment}]`;
240
+ } else if (field.length === 0) {
241
+ field = segment;
242
+ } else {
243
+ field += `.${segment}`;
244
+ }
245
+ }
246
+ return field;
247
+ }
248
+
249
+ function normalizeFieldName(value) {
250
+ return String(value).replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
251
+ }