Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
370db8d
Model canonical conversion mechanisms
HardlyDifficult Jul 10, 2026
9d0333e
Merge commit 'fee0edd' into codex/canonical-conversion-mechanisms
HardlyDifficult Jul 10, 2026
bafb311
Merge branch 'codex/schema-conformance-harness' into codex/canonical-…
HardlyDifficult Jul 10, 2026
80c7b76
Handle null optional issuance quantities
HardlyDifficult Jul 10, 2026
15e8d7b
Reject null canonical numeric fields
HardlyDifficult Jul 10, 2026
9def38f
Merge remote-tracking branch 'origin/codex/schema-conformance-harness…
HardlyDifficult Jul 10, 2026
f367e5b
Reject invalid warrant quantity sources
HardlyDifficult Jul 10, 2026
9a043bb
Satisfy exhaustive quantity source validation
HardlyDifficult Jul 10, 2026
aaa2afd
Merge remote-tracking branch 'origin/codex/schema-conformance-harness…
HardlyDifficult Jul 10, 2026
2eeab05
Reject blank capitalization definitions
HardlyDifficult Jul 10, 2026
615db2c
Expand capitalization definition matrix
HardlyDifficult Jul 10, 2026
ed7bc08
Enforce generated conversion boundaries
HardlyDifficult Jul 10, 2026
b3df6a8
Merge remote-tracking branch 'origin/codex/schema-conformance-harness…
HardlyDifficult Jul 10, 2026
dd2a413
Merge remote-tracking branch 'origin/codex/schema-conformance-harness…
HardlyDifficult Jul 10, 2026
a40f9c0
Merge remote-tracking branch 'origin/codex/schema-conformance-harness…
HardlyDifficult Jul 10, 2026
0a0d8a0
Merge schema conformance harness
HardlyDifficult Jul 10, 2026
a7b6b21
Merge remote-tracking branch 'origin/codex/schema-conformance-harness…
HardlyDifficult Jul 10, 2026
e1ef029
fix: validate optional PPS discount inputs
HardlyDifficult Jul 10, 2026
6812ffd
Merge schema conformance updates into conversion mechanisms
HardlyDifficult Jul 11, 2026
fecbf11
Merge nullable document fixes into conversion mechanisms
HardlyDifficult Jul 11, 2026
b6f9502
Merge PR #419 into canonical conversion mechanisms
HardlyDifficult Jul 11, 2026
fb7d3d3
fix: reject invalid issuance discriminants and IDs
HardlyDifficult Jul 11, 2026
f5aff43
fix: validate conversion numeric boundaries
HardlyDifficult Jul 11, 2026
f0f4496
test: cover seniority runtime scalar boundaries
HardlyDifficult Jul 11, 2026
cc7a210
fix: validate convertible seniority on write
HardlyDifficult Jul 11, 2026
1805659
fix: preserve numeric validation field paths
HardlyDifficult Jul 11, 2026
4d47d8d
Merge commit '9ddc48365e4c31b43c9eb9ccd203b61652332218' into codex/ca…
HardlyDifficult Jul 11, 2026
6795c0e
fix: preserve numeric readback field paths
HardlyDifficult Jul 11, 2026
2a40665
fix: attribute converter numeric errors
HardlyDifficult Jul 11, 2026
3e17a83
fix: preserve indexed numeric diagnostics
HardlyDifficult Jul 11, 2026
3c04009
fix: reject lossy converter fallbacks
HardlyDifficult Jul 11, 2026
8b92eb2
fix: index conversion diagnostic paths
HardlyDifficult Jul 11, 2026
f0ea2f2
fix: index unknown warrant rights
HardlyDifficult Jul 11, 2026
27a1db7
fix: reject remaining lossy conversion boundaries
HardlyDifficult Jul 11, 2026
247fcb4
fix: reject malformed stock class flags
HardlyDifficult Jul 11, 2026
c755916
fix: close conversion validation gaps
HardlyDifficult Jul 11, 2026
b7fa726
fix: classify conversion read errors
HardlyDifficult Jul 11, 2026
2c2f5b0
test: remove unsupported nickname assertion
HardlyDifficult Jul 11, 2026
f2ab97d
fix: validate conversion stock class targets
HardlyDifficult Jul 11, 2026
dc8d75f
checkpoint: harden conversion boundaries
HardlyDifficult Jul 11, 2026
2683b88
test: cover adversarial conversion boundaries
HardlyDifficult Jul 11, 2026
e92baa0
test: satisfy full conversion validation gate
HardlyDifficult Jul 11, 2026
0837e34
fix: enforce canonical v34 conversion semantics
HardlyDifficult Jul 11, 2026
66d68ac
fix: close remaining conversion type gaps
HardlyDifficult Jul 11, 2026
a2ae637
fix: validate initial shares as numeric 10
HardlyDifficult Jul 11, 2026
6b75a3e
fix: make DAML reads lossless
HardlyDifficult Jul 11, 2026
b43531c
Merge remote-tracking branch 'origin/codex/canonical-conversion-mecha…
HardlyDifficult Jul 11, 2026
f8dc414
test: keep losslessness regressions lint-clean
HardlyDifficult Jul 11, 2026
8de42c6
fix: enforce lossless OCF conversion reads
HardlyDifficult Jul 11, 2026
27e2363
fix: enforce lossless dedicated conversion reads
HardlyDifficult Jul 11, 2026
ab98785
fix: close final conversion validation gaps
HardlyDifficult Jul 11, 2026
6c30358
fix: harden conversion writer boundaries
HardlyDifficult Jul 11, 2026
8678ed5
fix: align ratio adjustment type with OCF
HardlyDifficult Jul 11, 2026
74e4f24
fix: reject noncanonical OCF arrays
HardlyDifficult Jul 11, 2026
2a3e558
fix: reject proxied conversion inputs
HardlyDifficult Jul 11, 2026
8c43abf
fix: bound validation diagnostics
HardlyDifficult Jul 11, 2026
df78ccd
fix: harden conversion descriptor boundaries
HardlyDifficult Jul 11, 2026
f3fa141
fix: bound dense array validation
HardlyDifficult Jul 11, 2026
83b3361
Merge prepared PR #419 into canonical conversion mechanisms
HardlyDifficult Jul 11, 2026
faf2519
Merge latest prepared PR #419 into canonical conversion mechanisms
HardlyDifficult Jul 11, 2026
3d124e5
Merge final schema-harness wrapper fix into canonical conversion mech…
HardlyDifficult Jul 11, 2026
2166996
Merge remote-tracking branch 'origin/codex/schema-conformance-harness…
HardlyDifficult Jul 11, 2026
fb6c2a0
fix: require conversion result security ids
HardlyDifficult Jul 11, 2026
82aec22
test: align conversion result diagnostics
HardlyDifficult Jul 11, 2026
783b2d0
Merge commit '05c178c6099df65b602aa2d29273dd2a971dd3ba' into codex/ca…
HardlyDifficult Jul 11, 2026
ec50312
Merge commit 'c278771b8e6eb8da66d939e4fc235a78328fd116' into codex/ca…
HardlyDifficult Jul 11, 2026
b1dcb7f
Merge commit '174590a8ab67d4f16e0e85f086de08e9caa2eeb6' into codex/ca…
HardlyDifficult Jul 11, 2026
a42473a
Merge commit 'f8ef7b1b7f96c3f97472c8ff2f66aa9f3f5f2450' into codex/ca…
HardlyDifficult Jul 11, 2026
0f61496
Merge commit '8df010283fbf616ef264200bcdbeb1ec0445d1f1' into codex/ca…
HardlyDifficult Jul 11, 2026
22e3b6f
fix: tighten canonical conversion boundaries
HardlyDifficult Jul 12, 2026
0c4e505
Merge commit 'e8a0042d991030792af3a7ad4032501eb41bd4c9' into codex/ca…
HardlyDifficult Jul 12, 2026
f3c5773
Merge commit '335b70b17f63642ea2cc95ef98dcd02a30f8308f' into codex/ca…
HardlyDifficult Jul 12, 2026
c22c0c5
Merge branch 'codex/schema-conformance-harness' into codex/canonical-…
HardlyDifficult Jul 12, 2026
b190981
fix: separate canonical and persisted valuations
HardlyDifficult Jul 12, 2026
bf3f0df
Merge branch 'codex/schema-conformance-harness' into codex/canonical-…
HardlyDifficult Jul 12, 2026
f7b9f65
docs: cite warrant valuation invariant
HardlyDifficult Jul 12, 2026
c97ddc3
Merge branch 'codex/schema-conformance-harness' into codex/canonical-…
HardlyDifficult Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 37 additions & 9 deletions src/errors/OcpError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface OcpErrorDetails {
}

const MAX_DIAGNOSTIC_STRING_LENGTH = 256;
const MAX_DIAGNOSTIC_TEXT_LENGTH = 768;
const MAX_DIAGNOSTIC_TEXT_LENGTH = 512;
const MAX_DIAGNOSTIC_CONTAINER_ITEMS = 12;
const MAX_DIAGNOSTIC_DEPTH = 6;
const MAX_DIAGNOSTIC_NODES = 48;
Expand Down Expand Up @@ -304,13 +304,41 @@ export class OcpError extends Error {

/** Return a globally bounded, JSON-safe representation for logs and telemetry. */
toJSON(): unknown {
return toSafeDiagnosticValue({
name: this.name,
code: this.code,
message: this.message,
classification: this.classification,
context: this.context,
...(this.cause !== undefined ? { cause: this.cause } : {}),
});
const ownDataValue = (key: string): unknown => {
const descriptor = Object.getOwnPropertyDescriptor(this, key);
return descriptor !== undefined && 'value' in descriptor ? descriptor.value : undefined;
};
const rawName = ownDataValue('name');
const rawMessage = ownDataValue('message');
const rawCode = ownDataValue('code');
const rawClassification = ownDataValue('classification');
const rawContext = ownDataValue('context');
const rawCause = ownDataValue('cause');
const result: Record<string, unknown> = {
name: typeof rawName === 'string' ? toSafeDiagnosticText(rawName) : 'OcpError',
message: typeof rawMessage === 'string' ? toSafeDiagnosticText(rawMessage) : 'OCP SDK error',
code: toSafeDiagnosticValue(rawCode),
...(typeof rawClassification === 'string'
? { classification: toSafeDiagnosticText(rawClassification, 256) }
: {}),
...(rawContext !== undefined ? { context: toSafeDiagnosticValue(rawContext) } : {}),
...(rawCause !== undefined ? { cause: toSafeDiagnosticValue(rawCause) } : {}),
};

for (const key of [
'fieldPath',
'expectedType',
'receivedValue',
'source',
'contractId',
'templateId',
'choice',
'endpoint',
'statusCode',
]) {
const value = ownDataValue(key);
if (value !== undefined) result[key] = toSafeDiagnosticValue(value);
}
return toSafeDiagnosticValue(result);
}
}
137 changes: 137 additions & 0 deletions src/errors/diagnosticValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { types as nodeUtilTypes } from 'node:util';

const MAX_DIAGNOSTIC_STRING_LENGTH = 256;
const MAX_DIAGNOSTIC_KEY_LENGTH = 64;
const MAX_DIAGNOSTIC_CONTAINER_ITEMS = 12;
const MAX_DIAGNOSTIC_DEPTH = 2;
const MAX_DIAGNOSTIC_NODES = 8;

interface DiagnosticState {
nodes: number;
readonly seen: WeakSet<object>;
}

function truncate(value: string, maximum = MAX_DIAGNOSTIC_STRING_LENGTH): string {
if (value.length <= maximum) return value;
const suffix = `...[${value.length} chars]`;
return `${value.slice(0, Math.max(0, maximum - suffix.length))}${suffix}`;
}

/** Bound text that may become part of an SDK diagnostic message. */
export function boundedDiagnosticText(value: string): string {
return truncate(value, 512);
}

/** Bound user-influenced paths while preserving ordinary field paths verbatim. */
export function boundedDiagnosticPath(value: string): string {
return truncate(value, 512);
}

/** Build a bounded child path without retaining an arbitrarily large property name. */
export function diagnosticPropertyPath(parent: string, property: string | symbol): string {
const boundedParent = boundedDiagnosticPath(parent);
if (typeof property === 'symbol') {
return boundedDiagnosticPath(`${boundedParent}[${truncate(String(property), 96)}]`);
}
const boundedProperty = truncate(property, 96);
const path = /^(?:0|[1-9]\d*|[A-Za-z_$][A-Za-z0-9_$]*)$/.test(boundedProperty)
? `${boundedParent}.${boundedProperty}`
: `${boundedParent}[${JSON.stringify(boundedProperty)}]`;
return boundedDiagnosticPath(path);
}

function metadata(type: string, details: Readonly<Record<string, string | number | boolean>> = {}): unknown {
return { type, ...details };
}

function summarize(value: unknown, depth: number, state: DiagnosticState): unknown {
if (value === null || value === undefined) return value;

const valueType = typeof value;
if (valueType === 'string') return truncate(value as string);
if (valueType === 'boolean') return value;
if (valueType === 'number') return value;
if (valueType === 'bigint') return metadata('bigint');
if (valueType === 'symbol') {
const symbolValue = value as symbol;
const { description } = symbolValue;
return metadata('symbol', description === undefined ? {} : { description: truncate(description) });
}

if (nodeUtilTypes.isProxy(value)) return metadata('proxy');
if (valueType === 'function') return metadata('function');
if (valueType !== 'object') return metadata(valueType);

const object = value;
if (state.seen.has(object)) return metadata('circular');
if (depth >= MAX_DIAGNOSTIC_DEPTH || state.nodes >= MAX_DIAGNOSTIC_NODES) {
return metadata(Array.isArray(object) ? 'array' : 'object');
}

state.nodes += 1;
state.seen.add(object);
try {
if (Array.isArray(object)) {
const { length } = object;
if (length > MAX_DIAGNOSTIC_CONTAINER_ITEMS) return metadata('array', { length });

const result: unknown[] = [];
for (let index = 0; index < length; index += 1) {
const descriptor = Object.getOwnPropertyDescriptor(object, String(index));
if (descriptor === undefined) {
result.push(metadata('array-hole'));
} else if ('value' in descriptor) {
result.push(summarize(descriptor.value, depth + 1, state));
} else {
result.push(metadata('accessor'));
}
}
return result;
}

const prototype = Object.getPrototypeOf(object) as object | null;
if (prototype !== Object.prototype && prototype !== null) return metadata('object');

const keys = Object.getOwnPropertyNames(object);
const symbols = Object.getOwnPropertySymbols(object);
if (keys.length > MAX_DIAGNOSTIC_CONTAINER_ITEMS || keys.some((key) => key.length > MAX_DIAGNOSTIC_KEY_LENGTH)) {
return metadata('object', { keys: keys.length, symbolKeys: symbols.length });
}

const result: Record<string, unknown> = {};
for (const key of keys) {
const descriptor = Object.getOwnPropertyDescriptor(object, key);
const summarized =
descriptor === undefined
? metadata('missing-property')
: 'value' in descriptor
? summarize(descriptor.value, depth + 1, state)
: metadata('accessor');
Object.defineProperty(result, key, { configurable: true, enumerable: true, value: summarized, writable: true });
}
if (symbols.length > 0) result.$symbolKeys = symbols.length;
return result;
} catch {
return metadata('uninspectable-object');
} finally {
state.seen.delete(object);
}
}

/**
* Convert arbitrary runtime input into a JSON-safe, size-bounded diagnostic.
* No getters, custom serializers, or Proxy traps are invoked.
*/
export function toBoundedDiagnosticValue(value: unknown): unknown {
return summarize(value, 0, { nodes: 0, seen: new WeakSet<object>() });
}

/** Convert arbitrary structured error context into a bounded JSON-safe record. */
export function toBoundedDiagnosticContext(value: unknown): Record<string, unknown> | undefined {
if (value === undefined) return undefined;
const summarized = toBoundedDiagnosticValue(value);
if (summarized === null || typeof summarized !== 'object' || Array.isArray(summarized)) {
return { value: summarized };
}
return summarized as Record<string, unknown>;
}
5 changes: 3 additions & 2 deletions src/functions/OpenCapTable/capTable/batchTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { Fairmint } from '@fairmint/open-captable-protocol-daml-js';
import type { OcfObjectType } from '../../../types/native';
import type {
OcfCreatableEntityType,
OcfDataTypeFor,
OcfDeletableEntityType,
OcfEditableEntityType,
OcfEntityDataMap,
OcfEntityType,
OcfWritableDataTypeFor,
} from './entityTypes';

export {
Expand All @@ -40,6 +40,7 @@ export {
type OcfEntityTypeForObjectType,
type OcfReadableDataForObjectType,
type OcfReadableObjectType,
type OcfWritableDataTypeFor,
} from './entityTypes';

// Re-export DAML types for convenience
Expand Down Expand Up @@ -500,7 +501,7 @@ export type DamlEntityArguments = {

/** Correlated entity-kind and native-data tuples accepted by the converter dispatcher. */
export type OcfEntityArguments = {
[EntityType in OcfEntityType]: readonly [type: EntityType, data: OcfDataTypeFor<EntityType>];
[EntityType in OcfEntityType]: readonly [type: EntityType, data: OcfWritableDataTypeFor<EntityType>];
}[OcfEntityType];

function entityRegistryEntries(): Array<[OcfEntityType, OcfEntityRegistryEntry]> {
Expand Down
Loading