@tiinex/core 0.3.0 → 0.5.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.
- package/package.json +8 -6
- package/src/public/index.js +21 -0
- package/src/public/node.js +1 -0
- package/src/release/plan.mjs +18 -5
- package/src/release/run.mjs +7 -5
- package/src/tooling/portable/adapters/cli/cli.command-input.js +3 -0
- package/src/tooling/portable/adapters/cli/cli.common-output.js +23 -0
- package/src/tooling/portable/adapters/cli/cli.handoff-manufacture.js +3 -1
- package/src/tooling/portable/adapters/cli/cli.help.js +18 -2
- package/src/tooling/portable/adapters/cli/cli.material-policy.js +0 -1
- package/src/tooling/portable/adapters/cli/cli.operator-bridge.js +0 -13
- package/src/tooling/portable/adapters/cli/cli.run.js +1 -1
- package/src/tooling/portable/adapters/cli/cli.source-frontier-comparison.js +50 -0
- package/src/tooling/portable/adapters/node/handoff.manufacture.js +14 -0
- package/src/tooling/portable/adapters/node/handoff.manufacture.packageParent.js +80 -16
- package/src/tooling/portable/adapters/node/handoff.manufacture.requirements.js +44 -11
- package/src/tooling/portable/adapters/node/handoff.manufacture.scope.js +96 -1
- package/src/tooling/portable/adapters/node/sourceFrontierComparison.js +194 -0
- package/src/tooling/portable/comparison/sourceFrontierComparison.js +501 -0
- package/src/tooling/portable/grounding/grounding.readiness.js +8 -4
- package/src/tooling/portable/grounding/grounding.readiness.support.js +63 -1
- package/src/tooling/portable/handoff/contextAudit.js +21 -1
- package/src/tooling/portable/handoff/materialClosure.descriptor.js +1 -1
- package/src/tooling/portable/handoff/materialClosure.materials.js +1 -1
- package/src/tooling/portable/handoff/recipientV2.artifacts.js +1 -1
- package/src/tooling/portable/handoff/recipientV2.inspect.helpers.js +6 -1
- package/src/tooling/portable/handoff/recipientV2.packageV1.build.js +93 -19
- package/src/tooling/portable/handoff/recipientV2.packageV1.contract.js +30 -1
- package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.helpers.js +8 -2
- package/src/tooling/portable/handoff/recipientV2.packageV1.inspect.js +122 -10
- package/src/tooling/portable/handoff/recipientV2.packageV1.js +2 -1
- package/src/tooling/portable/handoff/recipientV2.packageV1.secure.js +87 -0
- package/src/tooling/portable/handoff/recipientV2.packageV1.shared.js +9 -1
- package/src/tooling/portable/handoff/recipientV2.topology.js +2 -2
- package/src/tooling/portable/handoff/recipientV2.topology.materials.js +9 -1
- package/src/tooling/portable/handoff/transportEnvelopeV1.js +76 -0
- package/src/tooling/portable/index.js +4 -0
- package/src/tooling/portable/operation.catalog.js +8 -0
- package/src/tooling/portable/operation.catalog.package.js +0 -8
- package/src/transport/secureTransportV1.js +339 -0
- package/src/tooling/portable/handoff/sourceFrontierComparison.js +0 -186
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { packageFileByteView, packageFileBytes, sha256Hex } from '../../../export/package.bytes.js';
|
|
2
|
+
import { parseArtifactMarkdown } from '../../../artifacts/artifact.parse.js';
|
|
2
3
|
import { canonicalC14nV2SelfState } from '../../../integrity/integrity.c14nV2.js';
|
|
3
4
|
import { buildHandoffCarrierProjection } from './carrierProjection.js';
|
|
4
|
-
import { buildHandoffWorkspaceByteProvider, inspectStoredWorkspaceArchive } from './workspaceByteProvider.js';
|
|
5
|
+
import { buildHandoffWorkspaceByteProvider, inspectStoredWorkspaceArchive, resolveHandoffWorkspaceEntry } from './workspaceByteProvider.js';
|
|
6
|
+
import { parseWorkspaceQualifiedReference } from './workspaceQualifiedReference.js';
|
|
5
7
|
import { qualifyHandoffWorkspaceTarget } from './workspaceTargetConformance.js';
|
|
6
8
|
import { inspectRecipientV2Artifact, parseRecipientV2ExternalPayload, parseRecipientV2Facts } from './recipientV2.artifacts.js';
|
|
7
9
|
import { RECIPIENT_V2_READ_PATH } from './recipientV2.topology.js';
|
|
@@ -16,6 +18,7 @@ import { parseHandoffPackageV1, validatePackageFields, WORKSPACE_PACKAGE_ROLE }
|
|
|
16
18
|
import { deriveVisibleFacts, validateRouteClosure } from './recipientV2.packageV1.inspect.helpers.js';
|
|
17
19
|
import { workspaceCarrierProjection } from './recipientV2.packageV1.workspaceProjection.js';
|
|
18
20
|
import { byteEqual, currentSchemaId, decodeUtf8, dedupeFindings, deepFreeze, oneFile } from './recipientV2.packageV1.shared.js';
|
|
21
|
+
import { parseTransportEnvelopeV1, qualifyTransportEnvelopeV1Artifact, TRANSPORT_ENVELOPE_V1_ROLE, TRANSPORT_ENVELOPE_V1_SCHEMA_ID } from './transportEnvelopeV1.js';
|
|
19
22
|
|
|
20
23
|
export function inspectRecipientFacingV2PackageV1(bundle = {}, options = {}) {
|
|
21
24
|
const files = Array.isArray(bundle.files) ? bundle.files : [];
|
|
@@ -56,13 +59,39 @@ export function inspectRecipientFacingV2PackageV1(bundle = {}, options = {}) {
|
|
|
56
59
|
const workspaceParts = [];
|
|
57
60
|
const workspaceDescriptors = [];
|
|
58
61
|
const virtualWorkspaceTargets = [];
|
|
62
|
+
const sealedWorkspaceBindings = [];
|
|
59
63
|
for (const binding of bindings) {
|
|
60
64
|
const workspaceFile = oneFile(index, binding.workspaceArtifactPath);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const workspaceMarkdown = decodeUtf8(
|
|
65
|
+
if (!workspaceFile) { findings.push(finding('error', 'portable.handoff-package-v1.workspace-binding-unresolved', 'Workspace Artifact path must resolve exactly once inside the package.', { workspaceId: binding.workspaceId })); continue; }
|
|
66
|
+
const workspaceData = packageFileByteView(workspaceFile);
|
|
67
|
+
const workspaceMarkdown = decodeUtf8(workspaceData);
|
|
64
68
|
if (currentSchemaId(workspaceMarkdown) !== 'tiinex.workspace.v1') findings.push(finding('error', 'portable.handoff-package-v1.workspace-schema-invalid', 'Workspace Artifact must declare tiinex.workspace.v1.', { workspaceId: binding.workspaceId, path: workspaceFile.path || '' }));
|
|
65
|
-
|
|
69
|
+
|
|
70
|
+
if (String(binding.snapshotKind || '') === 'password-sealed-workspace-byte-tree') {
|
|
71
|
+
const payloadArtifact = generatedArtifacts.find((item) => item.path === binding.protectedPayloadDescriptorPath && item.schemaId === 'tiinex.external.payload.v1' && item.status === 'qualified') || null;
|
|
72
|
+
const envelopeArtifact = generatedArtifacts.find((item) => item.path === binding.transportEnvelopePath && item.schemaId === TRANSPORT_ENVELOPE_V1_SCHEMA_ID && item.status === 'qualified') || null;
|
|
73
|
+
if (!payloadArtifact || !envelopeArtifact) { findings.push(finding('error', 'portable.handoff-package-v1.workspace-sealed-binding-unresolved', 'Sealed Workspace binding descriptor and Transport Envelope must each resolve exactly once and qualify.', { workspaceId: binding.workspaceId })); continue; }
|
|
74
|
+
const payload = parseRecipientV2ExternalPayload(payloadArtifact.markdown);
|
|
75
|
+
const payloadFile = oneFile(index, payload.location);
|
|
76
|
+
if (!payloadFile || payload.integrityMethod !== 'sha256' || !/^[0-9a-f]{64}$/.test(String(payload.integrityValue || ''))) { findings.push(finding('error', 'portable.handoff-package-v1.workspace-sealed-payload-invalid', 'Protected External Payload descriptor must own one exact package-local ciphertext with SHA-256 integrity.', { workspaceId: binding.workspaceId })); continue; }
|
|
77
|
+
const protectedBytes = packageFileByteView(payloadFile);
|
|
78
|
+
if (sha256Hex(protectedBytes) !== payload.integrityValue || Number(payload.bytes || 0) !== protectedBytes.byteLength) findings.push(finding('error', 'portable.handoff-package-v1.workspace-sealed-payload-byte-mismatch', 'Protected payload descriptor byte identity diverges from exact carried ciphertext.', { workspaceId: binding.workspaceId }));
|
|
79
|
+
if (binding.byteSize !== null && Number(binding.byteSize) !== protectedBytes.byteLength) findings.push(finding('error', 'portable.handoff-package-v1.workspace-sealed-size-mismatch', 'Sealed binding Byte Size diverges from protected payload bytes.', { workspaceId: binding.workspaceId }));
|
|
80
|
+
const envelopeQualification = qualifyTransportEnvelopeV1Artifact(envelopeArtifact.markdown);
|
|
81
|
+
if (envelopeQualification.status !== 'qualified') findings.push(finding('error', 'portable.handoff-package-v1.workspace-sealed-envelope-invalid', 'Transport Envelope failed Secure Transport V1 qualification.', { workspaceId: binding.workspaceId, causes: envelopeQualification.findings || [] }));
|
|
82
|
+
const envelope = envelopeQualification.parsed || parseTransportEnvelopeV1(envelopeArtifact.markdown);
|
|
83
|
+
if (String(envelope.workspaceArtifactPath || '') !== String(binding.workspaceArtifactPath || '') || String(envelope.protectedPayloadDescriptorPath || '') !== String(binding.protectedPayloadDescriptorPath || '')) findings.push(finding('error', 'portable.handoff-package-v1.workspace-sealed-envelope-binding-mismatch', 'Transport Envelope must bind the same visible Workspace Artifact and protected External Payload descriptor as the package binding.', { workspaceId: binding.workspaceId }));
|
|
84
|
+
if (String(envelope.workspaceBindingValue || '') !== sha256Hex(workspaceData)) findings.push(finding('error', 'portable.handoff-package-v1.workspace-sealed-workspace-binding-mismatch', 'Transport Envelope Workspace Binding Value must equal SHA-256 of exact visible Workspace Artifact bytes.', { workspaceId: binding.workspaceId }));
|
|
85
|
+
if (String(payloadArtifact.facts?.workspaceId || '') && String(payloadArtifact.facts.workspaceId) !== String(binding.workspaceId || '')) findings.push(finding('error', 'portable.handoff-package-v1.workspace-sealed-payload-workspace-mismatch', 'Protected payload descriptor Workspace Id diverges from package binding.', { workspaceId: binding.workspaceId }));
|
|
86
|
+
workspaceParts.push({ workspaceId: binding.workspaceId, bindingState: 'sealed', artifact: Object.freeze({ path: workspaceFile.path, sha256: sha256Hex(workspaceData), markdown: workspaceMarkdown }), facts: { workspaceId: binding.workspaceId, sourceWorkspaceTargetInnerPath: '', sourceWorkspaceTargetSha256: sha256Hex(workspaceData) }, archiveFile: null, archive: null, targetQualification: null, protectedPayloadArtifact: payloadArtifact, protectedPayloadFile: payloadFile, transportEnvelopeArtifact: envelopeArtifact });
|
|
87
|
+
sealedWorkspaceBindings.push(Object.freeze({ workspaceId: binding.workspaceId, workspaceArtifactPath: binding.workspaceArtifactPath, workspaceArtifactSha256: sha256Hex(workspaceData), protectedPayloadDescriptorPath: binding.protectedPayloadDescriptorPath, protectedPayloadPath: payload.location, protectedPayloadSha256: payload.integrityValue, protectedPayloadBytes: protectedBytes.byteLength, transportEnvelopePath: binding.transportEnvelopePath, envelope }));
|
|
88
|
+
virtualWorkspaceTargets.push(workspaceFile);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const archiveFile = oneFile(index, binding.snapshotPath);
|
|
93
|
+
if (!archiveFile) { findings.push(finding('error', 'portable.handoff-package-v1.workspace-binding-unresolved', 'Clear Workspace Snapshot path must resolve exactly once inside the package.', { workspaceId: binding.workspaceId })); continue; }
|
|
94
|
+
if (String(binding.snapshotKind || '') !== 'exact-workspace-byte-tree-archive' || String(binding.coverage || '') !== 'complete' || String(binding.bindingState || '') !== 'verified' || String(binding.integrityMethod || '') !== 'sha256') findings.push(finding('error', 'portable.handoff-package-v1.workspace-binding-contract-invalid', 'Qualified clear package v1 Workspace binding requires exact complete verified sha256 semantics.', { workspaceId: binding.workspaceId }));
|
|
66
95
|
const archiveData = packageFileByteView(archiveFile);
|
|
67
96
|
const archiveSha = sha256Hex(archiveData);
|
|
68
97
|
if (archiveSha !== binding.integrityValue) findings.push(finding('error', 'portable.handoff-package-v1.workspace-snapshot-digest-mismatch', 'Workspace Snapshot Integrity Value diverges from exact carried bytes.', { workspaceId: binding.workspaceId }));
|
|
@@ -71,20 +100,20 @@ export function inspectRecipientFacingV2PackageV1(bundle = {}, options = {}) {
|
|
|
71
100
|
if (parsed.state !== 'qualified') { findings.push(finding('error', 'portable.handoff-package-v1.workspace-snapshot-invalid', 'Workspace Snapshot failed safe normalized archive qualification.', { workspaceId: binding.workspaceId })); continue; }
|
|
72
101
|
const inner = (parsed.entries || []).filter((entry) => entry.path === binding.workspaceArtifactInnerPath);
|
|
73
102
|
if (inner.length !== 1) { findings.push(finding('error', 'portable.handoff-package-v1.workspace-inner-unresolved', 'Workspace Artifact Inner Path must resolve exactly once.', { workspaceId: binding.workspaceId, count: inner.length })); continue; }
|
|
74
|
-
const workspaceData = packageFileByteView(workspaceFile);
|
|
75
103
|
const innerData = packageFileByteView({ data: inner[0].data });
|
|
76
104
|
if (!byteEqual(workspaceData, innerData)) findings.push(finding('error', 'portable.handoff-package-v1.workspace-inner-byte-mismatch', 'Carried Workspace Artifact bytes must exactly equal the selected snapshot inner entry.', { workspaceId: binding.workspaceId }));
|
|
77
105
|
const targetQualification = qualifyHandoffWorkspaceTarget({ targetPath: binding.workspaceArtifactInnerPath, targetData: workspaceData, entries: parsed.entries || [] });
|
|
78
106
|
if (targetQualification.state !== 'qualified') findings.push(finding('error', 'portable.handoff-package-v1.workspace-target-unqualified', 'Exact bound Workspace Artifact does not qualify.', { workspaceId: binding.workspaceId, reasons: targetQualification.reasons || [] }));
|
|
79
107
|
const descriptorPart = recipientWorkspaceDescriptor({ workspaceId: binding.workspaceId, facts: { providerKind: 'package-local-stored-zip-v1' }, representation: { workspaceArtifactInnerPath: binding.workspaceArtifactInnerPath, coverage: 'complete' }, payload: { location: binding.snapshotPath }, entries: parsed.entries || [], targetMarkdown: workspaceMarkdown, targetPackagePath: binding.workspaceArtifactPath, targetFile: { bytes: workspaceData.byteLength, sha256: sha256Hex(workspaceData) }, archiveFile: { path: archiveFile.path, bytes: archiveData.byteLength, sha256: archiveSha } });
|
|
80
108
|
workspaceDescriptors.push(descriptorPart);
|
|
81
|
-
workspaceParts.push({ workspaceId: binding.workspaceId, artifact: Object.freeze({ path: workspaceFile.path, sha256: sha256Hex(workspaceData), markdown: workspaceMarkdown }), facts: { workspaceId: binding.workspaceId, sourceWorkspaceTargetInnerPath: binding.workspaceArtifactInnerPath, sourceWorkspaceTargetSha256: sha256Hex(workspaceData) }, archiveFile, archive: { archive: parsed, sha256: archiveSha }, targetQualification });
|
|
109
|
+
workspaceParts.push({ workspaceId: binding.workspaceId, bindingState: 'verified', artifact: Object.freeze({ path: workspaceFile.path, sha256: sha256Hex(workspaceData), markdown: workspaceMarkdown }), facts: { workspaceId: binding.workspaceId, sourceWorkspaceTargetInnerPath: binding.workspaceArtifactInnerPath, sourceWorkspaceTargetSha256: sha256Hex(workspaceData) }, archiveFile, archive: { archive: parsed, sha256: archiveSha }, targetQualification });
|
|
82
110
|
virtualWorkspaceTargets.push(workspaceFile);
|
|
83
111
|
}
|
|
84
112
|
|
|
85
113
|
const payloadArtifacts = generatedArtifacts.filter((item) => item.schemaId === 'tiinex.external.payload.v1' && item.status === 'qualified');
|
|
86
114
|
const bootstrapArtifacts = payloadArtifacts.filter((item) => item.facts?.role === 'portable Tooling bootstrap runtime for recipient orientation and verification');
|
|
87
115
|
const cacheArtifacts = payloadArtifacts.filter((item) => item.facts?.role === 'workspace-scoped Handoff dependency cache');
|
|
116
|
+
const protectedWorkspacePayloadArtifacts = payloadArtifacts.filter((item) => item.facts?.role === 'password-sealed Workspace protected payload');
|
|
88
117
|
const forbiddenWorkspacePayloads = payloadArtifacts.filter((item) => item.facts?.role === 'workspace-representation-payload');
|
|
89
118
|
if (forbiddenWorkspacePayloads.length) findings.push(finding('error', 'portable.handoff-package-v1.workspace-payload-redundant', 'Complete package-local Workspace bindings must not carry redundant Workspace External Payload companions.', { count: forbiddenWorkspacePayloads.length }));
|
|
90
119
|
if (generatedArtifacts.some((item) => item.schemaId === 'tiinex.workspace.representation.v1' || item.schemaId === 'tiinex.relation.v1')) findings.push(finding('error', 'portable.handoff-package-v1.workspace-representation-redundant', 'Complete package-local Workspace bindings must not carry redundant Workspace Representation/Relation companions.'));
|
|
@@ -142,14 +171,21 @@ export function inspectRecipientFacingV2PackageV1(bundle = {}, options = {}) {
|
|
|
142
171
|
inspectRoutePointers(routePointers, carrierProjection, workspaceParts, endpointRolePointers, participantRolePointers, index, findings);
|
|
143
172
|
validateRouteClosure(routePointers, endpointRolePointers, participantRolePointers, caches, workspaceParts, findings);
|
|
144
173
|
if (routePointers.length !== 1) findings.push(finding('error', 'portable.handoff-package-v1.route-count-invalid', 'Qualified Handoff-carrier package-v1 delivery requires exactly one selected Handoff Pointer.', { count: routePointers.length }));
|
|
174
|
+
const sealedWorkspaceIds = new Set(sealedWorkspaceBindings.map((item) => String(item.workspaceId || '')));
|
|
145
175
|
for (const route of carrierProjection.routes || []) {
|
|
146
|
-
|
|
176
|
+
const unresolved = (route.requiredClosure?.requirements || []).filter((requirement) => requirement.state !== 'qualified');
|
|
177
|
+
const lockedOnly = unresolved.length > 0 && unresolved.every((requirement) => {
|
|
178
|
+
const qualified = parseWorkspaceQualifiedReference(String(requirement.referenceTarget || ''));
|
|
179
|
+
return qualified && sealedWorkspaceIds.has(String(qualified.workspaceId || ''));
|
|
180
|
+
});
|
|
181
|
+
if (route.requiredClosure?.state !== 'qualified' && !lockedOnly) findings.push(finding('error', 'portable.handoff-package-v1.required-closure-unqualified', 'Authoritative Handoff Required Context is neither qualified clear material nor an explicitly sealed locked Workspace binding.', { routeId: route.id || '' }));
|
|
147
182
|
for (const requirement of route.requiredClosure?.requirements || []) if (requirement.state === 'qualified' && !['workspace-archive-entry', 'materialized-required-material'].includes(String(requirement.resolution?.kind || ''))) findings.push(finding('error', 'portable.handoff-package-v1.external-closure-asset', 'Selected route closure requires a carrier kind not owned by complete Workspace snapshots or bounded cache.', { routeId: route.id || '', requirementId: requirement.requirementId || '', kind: requirement.resolution?.kind || '' }));
|
|
148
183
|
}
|
|
149
184
|
const allowedCacheRequirementIds = new Set();
|
|
150
185
|
for (const route of carrierProjection.routes || []) for (const requirement of route.requiredClosure?.requirements || []) if (requirement.requirementId) allowedCacheRequirementIds.add(String(requirement.requirementId));
|
|
151
186
|
for (const pointer of endpointRolePointers) if (pointer.facts?.endpointRequirementId) allowedCacheRequirementIds.add(String(pointer.facts.endpointRequirementId));
|
|
152
187
|
for (const pointer of participantRolePointers) if (pointer.facts?.participantRequirementId) allowedCacheRequirementIds.add(String(pointer.facts.participantRequirementId));
|
|
188
|
+
for (const requirementId of qualifyParentBoundaryCacheRequirementIds(caches, workspaceByteProvider, carrierProjection, findings)) allowedCacheRequirementIds.add(requirementId);
|
|
153
189
|
for (const cache of caches) for (const material of cache.facts?.materials || []) {
|
|
154
190
|
const requirementId = String(material.sourceRequirementId || material.requirementId || '');
|
|
155
191
|
if (!requirementId || !allowedCacheRequirementIds.has(requirementId)) findings.push(finding('error', 'portable.handoff-package-v1.cache-over-expansion', 'Workspace dependency cache contains material not required by the selected Handoff route closure.', { cache: cache.artifact.path, requirementId }));
|
|
@@ -159,7 +195,8 @@ export function inspectRecipientFacingV2PackageV1(bundle = {}, options = {}) {
|
|
|
159
195
|
const knownPaths = new Set([
|
|
160
196
|
packageFile?.path,
|
|
161
197
|
packageContract?.startPath,
|
|
162
|
-
...(bindings || []).flatMap((binding) => [binding.workspaceArtifactPath, binding.snapshotPath]),
|
|
198
|
+
...(bindings || []).flatMap((binding) => [binding.workspaceArtifactPath, binding.snapshotPath, binding.protectedPayloadDescriptorPath, binding.transportEnvelopePath]),
|
|
199
|
+
...protectedWorkspacePayloadArtifacts.flatMap((artifact) => [artifact.path, parseRecipientV2ExternalPayload(artifact.markdown).location]),
|
|
163
200
|
...generatedArtifacts.filter((item) => ['recovery-orientation', 'handoff-route', 'endpoint-role', 'participant-role'].includes(String(item.facts?.role || ''))).map((item) => item.path),
|
|
164
201
|
...bootstrapArtifacts.flatMap((artifact) => [artifact.path, parseRecipientV2ExternalPayload(artifact.markdown).location]),
|
|
165
202
|
...caches.flatMap((cache) => [cache.artifact.path, cache.file.path])
|
|
@@ -175,7 +212,7 @@ export function inspectRecipientFacingV2PackageV1(bundle = {}, options = {}) {
|
|
|
175
212
|
return deepFreeze({
|
|
176
213
|
schema: 'tiinex.portable.recipient-facing-handoff-package-v1.inspection.v1', detected: Boolean(packageFile), status, format: RECIPIENT_V2_PACKAGE_V1_FORMAT_ID,
|
|
177
214
|
rootArtifact: packageFile ? Object.freeze({ path: packageFile.path, schemaId: RECIPIENT_V2_PACKAGE_V1_SCHEMA_ID, sha256: sha256Hex(packageFileBytes(packageFile)), carrierLineage: lineage }) : null,
|
|
178
|
-
readArtifact, workspaces: Object.freeze(workspaceParts.map((item) => Object.freeze({ workspaceId: item.workspaceId, coverage: 'complete', workspaceArtifactPath: item.artifact.path, workspaceArchivePath: item.archiveFile
|
|
215
|
+
readArtifact, workspaces: Object.freeze(workspaceParts.map((item) => Object.freeze({ workspaceId: item.workspaceId, coverage: 'complete', bindingState: item.bindingState || 'verified', workspaceArtifactPath: item.artifact.path, workspaceArchivePath: item.archiveFile?.path || '', sourceWorkspaceTargetInnerPath: item.facts.sourceWorkspaceTargetInnerPath, sourceWorkspaceTargetSha256: item.facts.sourceWorkspaceTargetSha256 }))), sealedWorkspaces: Object.freeze(sealedWorkspaceBindings),
|
|
179
216
|
routes: projectRecipientV2Routes(routePointers, endpointRolePointers, participantRolePointers), endpointRoles: projectRecipientV2EndpointRoles(endpointRolePointers), participantRoles: projectRecipientV2ParticipantRoles(participantRolePointers),
|
|
180
217
|
caches: Object.freeze(caches.map((cache) => Object.freeze({ workspaceId: String(cache.facts?.workspaceId || ''), artifactPath: cache.artifact.path, archivePath: cache.file.path, materials: cache.facts.materials || [] }))),
|
|
181
218
|
bootstrapInspection, transportManifest: null, artifactFacts: Object.freeze(generatedArtifacts.map((item) => Object.freeze({ path: item.path, facts: item.facts }))), descriptor, workspaceByteProvider, carrierProjection, coldConsumerProjection,
|
|
@@ -183,3 +220,78 @@ export function inspectRecipientFacingV2PackageV1(bundle = {}, options = {}) {
|
|
|
183
220
|
boundary: 'Read-only qualification of tiinex.handoff.package.v1: visible package identity/discovery and complete package-local Workspace bindings are reverified from exact bytes; derived inventories have no authority.'
|
|
184
221
|
});
|
|
185
222
|
}
|
|
223
|
+
|
|
224
|
+
function qualifyParentBoundaryCacheRequirementIds(caches = [], workspaceByteProvider = {}, carrierProjection = {}, findings = []) {
|
|
225
|
+
const parentMaterials = [];
|
|
226
|
+
const targetIndex = new Map();
|
|
227
|
+
for (const cache of caches) for (const item of cache.facts?.materials || []) {
|
|
228
|
+
if (String(item.classification || '') !== 'parent-boundary') continue;
|
|
229
|
+
const entry = (cache.archive?.archive?.entries || []).find((candidate) => String(candidate.path || '') === String(item.archiveEntry || '')) || null;
|
|
230
|
+
const normalized = Object.freeze({ cache, item, entry, targetKey: `${String(item.targetWorkspaceId || '')}\0${normalizeWorkspacePath(item.targetPath || item.originalPath || '')}` });
|
|
231
|
+
parentMaterials.push(normalized);
|
|
232
|
+
const list = targetIndex.get(normalized.targetKey) || [];
|
|
233
|
+
list.push(normalized);
|
|
234
|
+
targetIndex.set(normalized.targetKey, list);
|
|
235
|
+
}
|
|
236
|
+
const allowed = new Set();
|
|
237
|
+
for (const candidate of parentMaterials) {
|
|
238
|
+
const item = candidate.item || {};
|
|
239
|
+
const requirementId = String(item.requirementId || '');
|
|
240
|
+
const routeWorkspaceId = String(item.routeWorkspaceId || '');
|
|
241
|
+
const routePath = normalizeWorkspacePath(item.routePath || '');
|
|
242
|
+
const sourceWorkspaceId = String(item.sourceWorkspaceId || '');
|
|
243
|
+
const sourcePath = normalizeWorkspacePath(item.sourcePath || '');
|
|
244
|
+
const targetWorkspaceId = String(item.targetWorkspaceId || '');
|
|
245
|
+
const targetPath = normalizeWorkspacePath(item.targetPath || item.originalPath || '');
|
|
246
|
+
const referenceTarget = String(item.referenceTarget || '').trim();
|
|
247
|
+
const routeQualified = (carrierProjection.routes || []).some((route) => String(route.state || '') === 'qualified' && String(route.workspaceId || '') === routeWorkspaceId && normalizeWorkspacePath(route.workspaceRelativePath || '') === routePath);
|
|
248
|
+
let reason = '';
|
|
249
|
+
if (!requirementId || !routeQualified || !sourceWorkspaceId || !sourcePath || !targetWorkspaceId || !targetPath || !referenceTarget || !candidate.entry) reason = 'parent-boundary-visible-facts-incomplete';
|
|
250
|
+
if (!reason && targetIndex.get(candidate.targetKey)?.length !== 1) reason = 'parent-boundary-target-ambiguous';
|
|
251
|
+
if (!reason) {
|
|
252
|
+
const alreadyCarried = resolveHandoffWorkspaceEntry(workspaceByteProvider, targetWorkspaceId, targetPath);
|
|
253
|
+
if (alreadyCarried.state === 'qualified') reason = 'parent-boundary-target-redundantly-carried';
|
|
254
|
+
}
|
|
255
|
+
let sourceData = null;
|
|
256
|
+
if (!reason) {
|
|
257
|
+
const sourceCarried = resolveHandoffWorkspaceEntry(workspaceByteProvider, sourceWorkspaceId, sourcePath);
|
|
258
|
+
if (sourceCarried.state === 'qualified') sourceData = sourceCarried.data;
|
|
259
|
+
else {
|
|
260
|
+
const sourceCandidates = targetIndex.get(`${sourceWorkspaceId}\0${sourcePath}`) || [];
|
|
261
|
+
if (sourceCandidates.length === 1 && sourceCandidates[0].entry) sourceData = sourceCandidates[0].entry.data;
|
|
262
|
+
else reason = 'parent-boundary-source-unresolved';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!reason) {
|
|
266
|
+
const markdown = decodeUtf8(packageFileBytes({ data: sourceData }));
|
|
267
|
+
let parent = null;
|
|
268
|
+
try { parent = parseArtifactMarkdown(markdown).envelope?.parent || null; } catch { parent = null; }
|
|
269
|
+
const declared = String(parent?.trace || (parent?.originEntries || []).find((entry) => String(entry?.label || '').trim() === 'relative')?.target || '').trim();
|
|
270
|
+
if (!declared || declared !== referenceTarget) reason = 'parent-boundary-reference-mismatch';
|
|
271
|
+
else {
|
|
272
|
+
const qualified = parseWorkspaceQualifiedReference(declared);
|
|
273
|
+
const expectedWorkspaceId = qualified ? String(qualified.workspaceId || '') : sourceWorkspaceId;
|
|
274
|
+
const expectedPath = qualified ? normalizeWorkspacePath(qualified.path || '') : resolveRelativeWorkspacePath(sourcePath, declared);
|
|
275
|
+
if (!expectedWorkspaceId || !expectedPath || expectedWorkspaceId !== targetWorkspaceId || expectedPath !== targetPath) reason = 'parent-boundary-target-mismatch';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (reason) findings.push(finding('error', 'portable.handoff-package-v1.cache-parent-boundary-unqualified', 'Workspace dependency cache Parent-boundary material is not independently justified by the selected route declared Parent chain.', { requirementId, reason }));
|
|
279
|
+
else allowed.add(requirementId);
|
|
280
|
+
}
|
|
281
|
+
return allowed;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function resolveRelativeWorkspacePath(sourcePath = '', target = '') {
|
|
285
|
+
let raw;
|
|
286
|
+
try { raw = decodeURIComponent(String(target || '').split('#')[0].split('?')[0]); } catch { return ''; }
|
|
287
|
+
if (!raw || raw.startsWith('/') || raw.startsWith('\\') || /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith('//')) return '';
|
|
288
|
+
const parts = normalizeWorkspacePath(sourcePath).split('/').slice(0, -1);
|
|
289
|
+
for (const part of raw.replace(/\\/g, '/').split('/')) {
|
|
290
|
+
if (!part || part === '.') continue;
|
|
291
|
+
if (part === '..') { if (!parts.length) return ''; parts.pop(); }
|
|
292
|
+
else parts.push(part);
|
|
293
|
+
}
|
|
294
|
+
return normalizeWorkspacePath(parts.join('/'));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function normalizeWorkspacePath(value = '') { return String(value || '').trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '').split('/').filter((part) => part && part !== '.').join('/'); }
|
|
@@ -3,8 +3,9 @@ import { RECIPIENT_V2_PACKAGE_V1_SCHEMA_ID } from './recipientV2.packageV1.const
|
|
|
3
3
|
import { currentSchemaId, decodeUtf8 } from './recipientV2.packageV1.shared.js';
|
|
4
4
|
|
|
5
5
|
export { RECIPIENT_V2_PACKAGE_V1_FORMAT_ID, RECIPIENT_V2_PACKAGE_V1_ROOT_PATH, RECIPIENT_V2_PACKAGE_V1_SCHEMA_ID, RECIPIENT_V2_PACKAGE_V1_SCHEMA_TARGET } from './recipientV2.packageV1.constants.js';
|
|
6
|
-
export { buildRecipientFacingV2PackageV1 } from './recipientV2.packageV1.build.js';
|
|
6
|
+
export { buildRecipientFacingV2PackageV1, buildRecipientFacingV2PackageV1Secure } from './recipientV2.packageV1.build.js';
|
|
7
7
|
export { inspectRecipientFacingV2PackageV1 } from './recipientV2.packageV1.inspect.js';
|
|
8
|
+
export { inspectRecipientV2PackageV1SealedBinding, openRecipientV2PackageV1SealedWorkspace, replaceRecipientV2PackageV1SealedWorkspaceRecipients } from './recipientV2.packageV1.secure.js';
|
|
8
9
|
export { parseHandoffPackageV1, renderHandoffPackageV1 } from './recipientV2.packageV1.contract.js';
|
|
9
10
|
|
|
10
11
|
export function isRecipientV2PackageV1Surface(files = []) {
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { packageFileByteView, packageFileBytes, sha256Hex } from '../../../export/package.bytes.js';
|
|
2
|
+
import { openPasswordWorkspacePayload, replacePasswordWorkspaceRecipients } from '../../../transport/secureTransportV1.js';
|
|
3
|
+
import { buildHandoffWorkspaceByteProvider, inspectStoredWorkspaceArchive } from './workspaceByteProvider.js';
|
|
4
|
+
import { qualifyHandoffWorkspaceTarget } from './workspaceTargetConformance.js';
|
|
5
|
+
import { indexRecipientFiles, recipientWorkspaceDescriptor } from './recipientV2.inspect.helpers.js';
|
|
6
|
+
import { parseRecipientV2ExternalPayload } from './recipientV2.artifacts.js';
|
|
7
|
+
import { inspectRecipientFacingV2PackageV1 } from './recipientV2.packageV1.inspect.js';
|
|
8
|
+
import { parseHandoffPackageV1 } from './recipientV2.packageV1.contract.js';
|
|
9
|
+
import { parseTransportEnvelopeV1 } from './transportEnvelopeV1.js';
|
|
10
|
+
import { currentSchemaId, decodeUtf8, oneFile } from './recipientV2.packageV1.shared.js';
|
|
11
|
+
|
|
12
|
+
export async function openRecipientV2PackageV1SealedWorkspace(bundle = {}, input = {}) {
|
|
13
|
+
const inspection = inspectRecipientFacingV2PackageV1(bundle);
|
|
14
|
+
if (inspection.status !== 'valid') return frozen({ state: 'failed', reason: 'package-unqualified', findings: inspection.findings || [] });
|
|
15
|
+
const workspaceId = String(input.workspaceId || '').trim();
|
|
16
|
+
const sealed = (inspection.sealedWorkspaces || []).filter((item) => String(item.workspaceId || '') === workspaceId);
|
|
17
|
+
if (sealed.length !== 1) return frozen({ state: 'locked', reason: sealed.length > 1 ? 'sealed-workspace-ambiguous' : 'sealed-workspace-unresolved' });
|
|
18
|
+
const target = sealed[0];
|
|
19
|
+
const files = Array.isArray(bundle.files) ? bundle.files : [];
|
|
20
|
+
const index = indexRecipientFiles(files, []);
|
|
21
|
+
const workspaceFile = oneFile(index, target.workspaceArtifactPath);
|
|
22
|
+
const payloadFile = oneFile(index, target.protectedPayloadPath);
|
|
23
|
+
const envelopeFile = oneFile(index, target.transportEnvelopePath);
|
|
24
|
+
if (!workspaceFile || !payloadFile || !envelopeFile) return frozen({ state: 'failed', reason: 'sealed-carrier-material-unresolved' });
|
|
25
|
+
const envelope = parseTransportEnvelopeV1(decodeUtf8(packageFileBytes(envelopeFile)));
|
|
26
|
+
const opened = await openPasswordWorkspacePayload({ protectedPayload: packageFileByteView(payloadFile), envelope, password: input.password, slotId: input.slotId, crypto: input.crypto });
|
|
27
|
+
if (opened.state !== 'opened') return opened;
|
|
28
|
+
|
|
29
|
+
const parsed = inspectStoredWorkspaceArchive(opened.plaintext, { ownedBytes: true });
|
|
30
|
+
if (parsed.state !== 'qualified') return frozen({ state: 'failed', reason: 'recovered-archive-unqualified', findings: parsed.findings || [] });
|
|
31
|
+
const workspaceBytes = packageFileByteView(workspaceFile);
|
|
32
|
+
const matches = (parsed.entries || []).filter((entry) => byteEqual(entry.data, workspaceBytes));
|
|
33
|
+
if (matches.length !== 1) return frozen({ state: 'failed', reason: matches.length > 1 ? 'workspace-artifact-correlation-ambiguous' : 'workspace-artifact-correlation-unresolved', matchCount: matches.length });
|
|
34
|
+
const innerPath = String(matches[0].path || '');
|
|
35
|
+
const targetQualification = qualifyHandoffWorkspaceTarget({ targetPath: innerPath, targetData: workspaceBytes, entries: parsed.entries || [] });
|
|
36
|
+
if (targetQualification.state !== 'qualified') return frozen({ state: 'failed', reason: 'recovered-workspace-unqualified', findings: targetQualification.reasons || [] });
|
|
37
|
+
|
|
38
|
+
const transientArchivePath = `tiinex-transient-opened-${safeToken(workspaceId)}.workspace.zip`;
|
|
39
|
+
const transientArchiveFile = frozen({ path: transientArchivePath, kind: 'transient-opened-workspace-archive', mediaType: 'application/zip', data: opened.plaintext, bytes: opened.plaintext.byteLength, sha256: sha256Hex(opened.plaintext) });
|
|
40
|
+
const descriptorPart = recipientWorkspaceDescriptor({
|
|
41
|
+
workspaceId,
|
|
42
|
+
facts: { providerKind: 'package-local-stored-zip-v1' },
|
|
43
|
+
representation: { workspaceArtifactInnerPath: innerPath, coverage: 'complete' },
|
|
44
|
+
payload: { location: transientArchivePath },
|
|
45
|
+
entries: parsed.entries || [],
|
|
46
|
+
targetMarkdown: decodeUtf8(workspaceBytes),
|
|
47
|
+
targetPackagePath: target.workspaceArtifactPath,
|
|
48
|
+
targetFile: { bytes: workspaceBytes.byteLength, sha256: sha256Hex(workspaceBytes) },
|
|
49
|
+
archiveFile: { path: transientArchivePath, bytes: opened.plaintext.byteLength, sha256: sha256Hex(opened.plaintext) }
|
|
50
|
+
});
|
|
51
|
+
const descriptor = frozen({ schema: 'tiinex.transport.handoff-material-closure-descriptor.v2', version: 2, workspaceMaterializations: [descriptorPart.workspace], workspaceArchiveBindings: [descriptorPart.binding], materialized: [], requirements: { required: [], reference: [], endpointRoles: [], participantRoles: [], dependencies: [] } });
|
|
52
|
+
const provider = buildHandoffWorkspaceByteProvider({ ...bundle, files: frozen([...files, transientArchiveFile]) }, descriptor);
|
|
53
|
+
if (provider.status !== 'ready') return frozen({ state: 'failed', reason: 'recovered-provider-unqualified', findings: provider.findings || [] });
|
|
54
|
+
return frozen({
|
|
55
|
+
state: 'opened-qualified', workspaceId, slotId: opened.slotId, workspaceArtifactPath: target.workspaceArtifactPath,
|
|
56
|
+
workspaceArtifactInnerPath: innerPath, workspaceArtifactSha256: sha256Hex(workspaceBytes), archiveSha256: sha256Hex(opened.plaintext),
|
|
57
|
+
provider, descriptor, targetQualification,
|
|
58
|
+
boundary: 'Authorized plaintext exists only in this in-memory opened provider/descriptor result unless the caller explicitly writes a destination. Decryption alone did not activate the provider; safe archive, unique Workspace-byte correlation, ordinary Workspace conformance, and integrity qualification all completed first.'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function replaceRecipientV2PackageV1SealedWorkspaceRecipients(bundle = {}, input = {}) {
|
|
63
|
+
const inspection = inspectRecipientFacingV2PackageV1(bundle);
|
|
64
|
+
if (inspection.status !== 'valid') return frozen({ state: 'failed', reason: 'package-unqualified', findings: inspection.findings || [] });
|
|
65
|
+
const workspaceId = String(input.workspaceId || '').trim();
|
|
66
|
+
const target = (inspection.sealedWorkspaces || []).find((item) => String(item.workspaceId || '') === workspaceId) || null;
|
|
67
|
+
if (!target) return frozen({ state: 'locked', reason: 'sealed-workspace-unresolved' });
|
|
68
|
+
const index = indexRecipientFiles(bundle.files || [], []);
|
|
69
|
+
const payloadFile = oneFile(index, target.protectedPayloadPath);
|
|
70
|
+
const envelopeFile = oneFile(index, target.transportEnvelopePath);
|
|
71
|
+
if (!payloadFile || !envelopeFile) return frozen({ state: 'failed', reason: 'sealed-carrier-material-unresolved' });
|
|
72
|
+
const envelope = parseTransportEnvelopeV1(decodeUtf8(packageFileBytes(envelopeFile)));
|
|
73
|
+
const changed = await replacePasswordWorkspaceRecipients({ protectedPayload: packageFileByteView(payloadFile), envelope, authorizationPassword: input.authorizationPassword ?? input.password, recipients: input.recipients || [], crypto: input.crypto });
|
|
74
|
+
if (changed.state !== 'rewrapped') return changed;
|
|
75
|
+
return frozen({ state: 'rewrapped', protectedPayload: changed.protectedPayload, envelope: changed.envelope, workspaceId, protectedPayloadSha256: sha256Hex(changed.protectedPayload), boundary: 'Returns replacement non-secret envelope metadata and the exact unchanged protected payload bytes; it does not rewrite the package automatically because parent/self-integrity resealing remains a separate carrier manufacture action.' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function inspectRecipientV2PackageV1SealedBinding(bundle = {}, workspaceId = '') {
|
|
79
|
+
const inspection = inspectRecipientFacingV2PackageV1(bundle);
|
|
80
|
+
const matches = (inspection.sealedWorkspaces || []).filter((item) => String(item.workspaceId || '') === String(workspaceId || ''));
|
|
81
|
+
return frozen({ state: inspection.status !== 'valid' ? 'failed' : matches.length === 1 ? 'locked-qualified' : matches.length > 1 ? 'ambiguous' : 'unresolved', binding: matches.length === 1 ? matches[0] : null, providerActive: false, packageStatus: inspection.status, findings: inspection.findings || [] });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function byteEqual(a, b) { const left=packageFileByteView({data:a}), right=packageFileByteView({data:b}); if(left.byteLength!==right.byteLength)return false; let diff=0; for(let i=0;i<left.byteLength;i+=1) diff|=left[i]^right[i]; return diff===0; }
|
|
85
|
+
function safeToken(value=''){return String(value||'').trim().toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,80)||'workspace';}
|
|
86
|
+
function frozen(value){return deepFreeze(value);}
|
|
87
|
+
function deepFreeze(value){if(!value||typeof value!=='object'||Object.isFrozen(value))return value;if(ArrayBuffer.isView(value)||value instanceof ArrayBuffer)return value;for(const child of Object.values(value))deepFreeze(child);return Object.freeze(value);}
|
|
@@ -12,7 +12,15 @@ export function oneFile(index, path) { const list = index.get(String(path || '')
|
|
|
12
12
|
export function validCarrierDimension(value = '') { return /^\d{3}(?:-(?:[1-9]\d*))*$/.test(String(value || '')); }
|
|
13
13
|
export function numericDimension(path = '') { return String(path || '').match(/^(\d{3}(?:-[1-9]\d*)*)-/)?.[1] || ''; }
|
|
14
14
|
export function currentSchemaId(markdown = '') { return String(markdown || '').match(/^\s*-\s+Current Schema:\s*(?:\[)?(tiinex\.[A-Za-z0-9._-]+)(?:\])?/mi)?.[1] || ''; }
|
|
15
|
-
export function sectionText(markdown = '', title = '') {
|
|
15
|
+
export function sectionText(markdown = '', title = '') {
|
|
16
|
+
const source = String(markdown || '');
|
|
17
|
+
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18
|
+
const heading = new RegExp(`^##\\s+${escaped}\\s*$`, 'mi').exec(source);
|
|
19
|
+
if (!heading) return '';
|
|
20
|
+
const rest = source.slice(heading.index + heading[0].length);
|
|
21
|
+
const next = /^(?:##\s+|#\s+Continuity Integrity\s*$)/mi.exec(rest);
|
|
22
|
+
return next ? rest.slice(0, next.index) : rest;
|
|
23
|
+
}
|
|
16
24
|
export function field(section = '', label = '') { const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return String(section || '').match(new RegExp(`^\\s*-\\s+${escaped}:\\s*(.*?)\\s*$`, 'mi'))?.[1]?.trim() || ''; }
|
|
17
25
|
export function markdownTarget(value = '') { const match = String(value || '').match(/\[[^\]]*\]\(([^)]+)\)/); return match ? match[1].trim() : ''; }
|
|
18
26
|
export function unquote(value = '') { const text = String(value || '').trim(); return text.startsWith('`') && text.endsWith('`') ? text.slice(1, -1) : text; }
|
|
@@ -157,9 +157,9 @@ function buildRecipientFacingV2TopologyLegacy(input = {}) {
|
|
|
157
157
|
archivePath,
|
|
158
158
|
archiveBytes: cacheFile.bytes,
|
|
159
159
|
archiveSha256: cacheFile.sha256,
|
|
160
|
-
materials: materials.map((item, index) => ({ requirementId: item.requirementId, classification: item.classification, referenceTarget: item.referenceTarget, routeWorkspaceId: item.routeWorkspaceId, routePath: item.routePath, sourceRequirementId: item.sourceRequirementId, originalPath: item.originalPath, archiveEntry: cacheEntries[index].path, bytes: item.bytes, sha256: item.sha256 }))
|
|
160
|
+
materials: materials.map((item, index) => ({ requirementId: item.requirementId, classification: item.classification, referenceTarget: item.referenceTarget, routeWorkspaceId: item.routeWorkspaceId, routePath: item.routePath, sourceRequirementId: item.sourceRequirementId, sourceWorkspaceId: item.sourceWorkspaceId, sourcePath: item.sourcePath, targetWorkspaceId: item.targetWorkspaceId, targetPath: item.targetPath, originalPath: item.originalPath, archiveEntry: cacheEntries[index].path, bytes: item.bytes, sha256: item.sha256 }))
|
|
161
161
|
};
|
|
162
|
-
const cacheArtifact = finalizeFile({ path: artifactPath, kind: 'tiinex-external-payload-artifact', logicalKind: 'recipient-v2-workspace-dependency-cache-reference', mediaType: 'text/markdown', transportFacts: recipientV2TransportFacts('workspace-scoped Handoff dependency cache', cacheFacts), content: renderRecipientV2ExternalPayload({ createdAt, parent: workspace.parent, title: `Workspace Dependency Cache — ${workspace.workspaceId}`, summary: 'Exact recipient-relative dependency bytes not satisfied by any qualified Workspace archive.', label: `${workspace.workspaceId} Handoff dependency cache`, kind: 'zip export', role: 'workspace-scoped Handoff dependency cache', location: archivePath, bytes: cacheFile.bytes, sha256: cacheFile.sha256,
|
|
162
|
+
const cacheArtifact = finalizeFile({ path: artifactPath, kind: 'tiinex-external-payload-artifact', logicalKind: 'recipient-v2-workspace-dependency-cache-reference', mediaType: 'text/markdown', transportFacts: recipientV2TransportFacts('workspace-scoped Handoff dependency cache', cacheFacts), content: renderRecipientV2ExternalPayload({ createdAt, parent: workspace.parent, title: `Workspace Dependency Cache — ${workspace.workspaceId}`, summary: 'Exact recipient-relative dependency bytes not satisfied by any qualified Workspace archive.', label: `${workspace.workspaceId} Handoff dependency cache`, kind: 'zip export', role: 'workspace-scoped Handoff dependency cache', location: archivePath, bytes: cacheFile.bytes, sha256: cacheFile.sha256, materials: cacheFacts.materials }) });
|
|
163
163
|
files.push(cacheArtifact, cacheFile);
|
|
164
164
|
const projection = Object.freeze({ workspaceId: workspace.workspaceId, artifactPath, archivePath, materials: cacheFacts.materials });
|
|
165
165
|
topology.caches.push(projection);
|
|
@@ -47,6 +47,13 @@ export function roleMaterialTarget(requirement = {}, descriptor = {}, workspaceB
|
|
|
47
47
|
|
|
48
48
|
export function detachedMaterial(descriptor, byPath, findings) {
|
|
49
49
|
const out = [];
|
|
50
|
+
const requirementById = new Map([
|
|
51
|
+
...(descriptor.requirements?.required || []),
|
|
52
|
+
...(descriptor.requirements?.reference || []),
|
|
53
|
+
...(descriptor.requirements?.endpointRoles || []),
|
|
54
|
+
...(descriptor.requirements?.participantRoles || []),
|
|
55
|
+
...(descriptor.requirements?.dependencies || [])
|
|
56
|
+
].map((item) => [String(item.requirementId || ''), item]));
|
|
50
57
|
for (const material of descriptor.materialized || []) {
|
|
51
58
|
if (String(material.carrierKind || '') === 'workspace-archive-entry') continue;
|
|
52
59
|
const file = oneFile(byPath, material.packagePath, findings, 'detached-material');
|
|
@@ -54,7 +61,8 @@ export function detachedMaterial(descriptor, byPath, findings) {
|
|
|
54
61
|
const data = packageFileBytes(file);
|
|
55
62
|
const sha256 = sha256Hex(data);
|
|
56
63
|
if (Number(material.bytes || 0) !== data.byteLength || String(material.sha256 || '') !== sha256) findings.push(finding('error', 'portable.handoff-v2-surface.cache.material-identity-mismatch', 'Detached material bytes diverge from qualified closure identity.', { requirementId: material.requirementId || '' }));
|
|
57
|
-
|
|
64
|
+
const requirement = requirementById.get(String(material.requirementId || '')) || {};
|
|
65
|
+
out.push(Object.freeze({ requirementId: String(material.requirementId || ''), classification: String(material.classification || ''), referenceTarget: String(material.referenceTarget || ''), routeWorkspaceId: String(material.routeWorkspaceId || ''), routePath: String(material.routePath || ''), sourceRequirementId: String(material.sourceRequirementId || ''), sourceWorkspaceId: String(requirement.sourceWorkspaceId || ''), sourcePath: String(requirement.sourcePath || ''), targetWorkspaceId: String(requirement.targetWorkspaceId || material.provenance?.workspaceId || ''), targetPath: String(requirement.targetPath || material.originalPath || ''), originalPath: String(material.originalPath || ''), bytes: data.byteLength, sha256, data }));
|
|
58
66
|
}
|
|
59
67
|
return out.sort((a, b) => a.requirementId.localeCompare(b.requirementId));
|
|
60
68
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { canonicalC14nV2SelfState, sealC14nV2Self } from '../../../integrity/integrity.c14nV2.js';
|
|
2
|
+
import { C14N_V2_VALIDATOR_TARGET } from '../../../integrity/integrity.methodReference.js';
|
|
3
|
+
import { RECIPIENT_V2_ROOT_SCHEMA_TARGET } from './recipientV2.artifacts.js';
|
|
4
|
+
import { field, markdownTarget, sectionText, unquote } from './recipientV2.packageV1.shared.js';
|
|
5
|
+
import { qualifySecureTransportV1Envelope } from '../../../transport/secureTransportV1.js';
|
|
6
|
+
|
|
7
|
+
export const TRANSPORT_ENVELOPE_V1_SCHEMA_ID = 'tiinex.transport.envelope.v1';
|
|
8
|
+
export const TRANSPORT_ENVELOPE_V1_ROLE = 'password-sealed-workspace-transport-envelope';
|
|
9
|
+
|
|
10
|
+
export function renderTransportEnvelopeV1(input = {}) {
|
|
11
|
+
const envelope = input.envelope || {};
|
|
12
|
+
const q = qualifySecureTransportV1Envelope(envelope);
|
|
13
|
+
if (q.state !== 'qualified') throw new Error(`secure-transport.envelope-unqualified:${q.state}`);
|
|
14
|
+
const workspacePath = String(input.workspaceArtifactPath || '').trim();
|
|
15
|
+
const payloadPath = String(input.protectedPayloadDescriptorPath || '').trim();
|
|
16
|
+
if (!workspacePath || !payloadPath) throw new Error('secure-transport.envelope-binding-path-missing');
|
|
17
|
+
const profile = envelope.profile || {};
|
|
18
|
+
const slots = (envelope.passwordRecipientSlots || []).map((slot) => `- ${slot.slotId}\n - Slot Id: ${slot.slotId}\n - Slot Kind: ${slot.slotKind}\n - KDF Algorithm: ${slot.kdfAlgorithm}\n - KDF Salt Encoding: ${slot.kdfSaltEncoding}\n - KDF Salt: ${slot.kdfSalt}\n - KDF Parameters: \`${stableJson(slot.kdfParameters)}\`\n - Key Wrap Algorithm: ${slot.keyWrapAlgorithm}\n - Key Wrap Parameters: \`${stableJson(slot.keyWrapParameters)}\`\n - Wrapped Content Key Encoding: ${slot.wrappedContentKeyEncoding}\n - Wrapped Content Key: ${slot.wrappedContentKey}\n - Slot Verification Rule: ${slot.slotVerificationRule}${slot.recipientHint ? `\n - Recipient Hint: ${slot.recipientHint}` : ''}`).join('\n');
|
|
19
|
+
const createdAt = normalizeCreatedAt(input.createdAt || '');
|
|
20
|
+
const unsigned = `# Continuity Context\n\n- Envelope Schema: [tiinex.root.v1](${RECIPIENT_V2_ROOT_SCHEMA_TARGET})\n${renderParentEnvelope(input.parent)}- Current\n - Current Schema: ${TRANSPORT_ENVELOPE_V1_SCHEMA_ID}\n - Created At: ${createdAt}\n - Summary: Password-sealed Workspace transport envelope with explicit supported profile metadata and independently usable password recipient slots.\n\n---\n\n# Transport Envelope\n\n## Envelope Binding\n\n- Workspace Artifact: [Workspace](${workspacePath})\n- Protected Payload: [Protected Payload](${payloadPath})\n- Envelope Purpose: ${envelope.envelopePurpose}\n- Envelope Version: ${envelope.envelopeVersion}\n- Plaintext Representation Kind: ${envelope.plaintextRepresentationKind}\n- Protected Name Tree: ${envelope.protectedNameTree}\n- Workspace Binding Method: ${envelope.workspaceBindingMethod}\n- Workspace Binding Value: ${envelope.workspaceBindingValue}\n- Content Key Scope: ${envelope.contentKeyScope}\n\n## Payload Protection Profile\n\n- Profile Id: ${profile.profileId}\n- Profile Version: ${profile.profileVersion}\n- Content Encryption Algorithm: ${profile.contentEncryptionAlgorithm}\n- Content Encryption Parameters: \`${stableJson(profile.contentEncryptionParameters)}\`\n- Nonce Or IV Encoding: ${profile.nonceOrIvEncoding}\n- Nonce Or IV: ${profile.nonceOrIv}\n- Payload Authentication Rule: ${profile.payloadAuthenticationRule}\n- Security Metadata Authentication Rule: ${profile.securityMetadataAuthenticationRule}\n- Recipient Change Payload Rule: ${profile.recipientChangePayloadRule}\n\n## Password Recipient Slots\n\n${slots}\n\n## Open And Recovery Contract\n\n- Open Rule: ${envelope.openRule}\n- Wrong Password Result: ${envelope.wrongPasswordResult}\n- Unsupported Profile Result: ${envelope.unsupportedProfileResult}\n- Malformed Metadata Result: ${envelope.malformedMetadataResult}\n- Authentication Failure Result: ${envelope.authenticationFailureResult}\n- Missing Authorized Slot Result: ${envelope.missingAuthorizedSlotResult}\n- Secret Persistence: ${envelope.secretPersistence}\n- Plaintext Persistence: ${envelope.plaintextPersistence}\n- Recovery Rule: ${envelope.recoveryRule}\n- Failure Policy: ${envelope.failurePolicy}\n- Multi-Workspace Isolation: ${envelope.multiWorkspaceIsolation}\n\n## Qualification Boundary\n\n- Payload Byte Integrity Owner: ${envelope.payloadByteIntegrityOwner}\n- Envelope Root Integrity Meaning: ${envelope.envelopeRootIntegrityMeaning}\n- Cryptographic Authentication Meaning: ${envelope.cryptographicAuthenticationMeaning}\n- Provider State While Locked: ${envelope.providerStateWhileLocked}\n- Post-Open Qualification: ${envelope.postOpenQualification}\n- Semantic Authority: ${envelope.semanticAuthority}\n\n## Disclosure Boundary\n\n- Outer Visible Material: ${envelope.outerVisibleMaterial}\n- Must Remain Sealed: ${envelope.mustRemainSealed}\n- Forbidden Durable Secrets: ${envelope.forbiddenDurableSecrets}\n- Privacy Policy Owner: ${envelope.privacyPolicyOwner}\n\n## Interpretation Limits\n\n- Encryption and successful open do not create Workspace identity, Handoff authority, provenance, semantic truth, acceptance, or provider qualification.\n- Passwords, derived keys, wrapping keys, plaintext content keys, and protected Workspace path inventory are not durable envelope material.\n\n---\n\n# Continuity Integrity\n\n${renderParentIntegrity(input.parent)}- [sha256-base64url-c14n-v2](${C14N_V2_VALIDATOR_TARGET})\n - Towards: self\n - Value: pending\n`;
|
|
21
|
+
const sealed = sealC14nV2Self(unsigned);
|
|
22
|
+
if (sealed.state !== 'sealed') throw new Error(`secure-transport.envelope-self-seal-failed:${sealed.reason || sealed.state}`);
|
|
23
|
+
return `${sealed.markdown}\n`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseTransportEnvelopeV1(markdown = '') {
|
|
27
|
+
const body = String(markdown || '');
|
|
28
|
+
const binding = sectionText(body, 'Envelope Binding');
|
|
29
|
+
const profile = sectionText(body, 'Payload Protection Profile');
|
|
30
|
+
const open = sectionText(body, 'Open And Recovery Contract');
|
|
31
|
+
const qualification = sectionText(body, 'Qualification Boundary');
|
|
32
|
+
const disclosure = sectionText(body, 'Disclosure Boundary');
|
|
33
|
+
return deepFreeze({
|
|
34
|
+
workspaceArtifactPath: markdownTarget(field(binding, 'Workspace Artifact')),
|
|
35
|
+
protectedPayloadDescriptorPath: markdownTarget(field(binding, 'Protected Payload')),
|
|
36
|
+
envelopePurpose: field(binding, 'Envelope Purpose'), envelopeVersion: numberField(binding, 'Envelope Version'), plaintextRepresentationKind: field(binding, 'Plaintext Representation Kind'), protectedNameTree: field(binding, 'Protected Name Tree'), workspaceBindingMethod: field(binding, 'Workspace Binding Method'), workspaceBindingValue: field(binding, 'Workspace Binding Value'), contentKeyScope: field(binding, 'Content Key Scope'),
|
|
37
|
+
profile: {
|
|
38
|
+
profileId: field(profile, 'Profile Id'), profileVersion: numberField(profile, 'Profile Version'), contentEncryptionAlgorithm: field(profile, 'Content Encryption Algorithm'), contentEncryptionParameters: jsonObjectField(profile, 'Content Encryption Parameters'), nonceOrIvEncoding: field(profile, 'Nonce Or IV Encoding'), nonceOrIv: field(profile, 'Nonce Or IV'), payloadAuthenticationRule: field(profile, 'Payload Authentication Rule'), securityMetadataAuthenticationRule: field(profile, 'Security Metadata Authentication Rule'), recipientChangePayloadRule: field(profile, 'Recipient Change Payload Rule')
|
|
39
|
+
},
|
|
40
|
+
passwordRecipientSlots: parseSlots(sectionText(body, 'Password Recipient Slots')),
|
|
41
|
+
openRule: field(open, 'Open Rule'), wrongPasswordResult: field(open, 'Wrong Password Result'), unsupportedProfileResult: field(open, 'Unsupported Profile Result'), malformedMetadataResult: field(open, 'Malformed Metadata Result'), authenticationFailureResult: field(open, 'Authentication Failure Result'), missingAuthorizedSlotResult: field(open, 'Missing Authorized Slot Result'), secretPersistence: field(open, 'Secret Persistence'), plaintextPersistence: field(open, 'Plaintext Persistence'), recoveryRule: field(open, 'Recovery Rule'), failurePolicy: field(open, 'Failure Policy'), multiWorkspaceIsolation: field(open, 'Multi-Workspace Isolation'),
|
|
42
|
+
payloadByteIntegrityOwner: field(qualification, 'Payload Byte Integrity Owner'), envelopeRootIntegrityMeaning: field(qualification, 'Envelope Root Integrity Meaning'), cryptographicAuthenticationMeaning: field(qualification, 'Cryptographic Authentication Meaning'), providerStateWhileLocked: field(qualification, 'Provider State While Locked'), postOpenQualification: field(qualification, 'Post-Open Qualification'), semanticAuthority: field(qualification, 'Semantic Authority'),
|
|
43
|
+
outerVisibleMaterial: field(disclosure, 'Outer Visible Material'), mustRemainSealed: field(disclosure, 'Must Remain Sealed'), forbiddenDurableSecrets: field(disclosure, 'Forbidden Durable Secrets'), privacyPolicyOwner: field(disclosure, 'Privacy Policy Owner')
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function qualifyTransportEnvelopeV1Artifact(markdown = '') {
|
|
48
|
+
const parsed = parseTransportEnvelopeV1(markdown);
|
|
49
|
+
const findings = [];
|
|
50
|
+
const self = canonicalC14nV2SelfState(String(markdown || ''));
|
|
51
|
+
if (self.state !== 'verified') findings.push({ severity: 'error', code: 'secure-transport.envelope.integrity-self-invalid', message: 'Transport Envelope self-integrity does not verify.', reason: self.reason || self.state });
|
|
52
|
+
const crypto = qualifySecureTransportV1Envelope(parsed);
|
|
53
|
+
for (const item of crypto.findings || []) findings.push({ severity: item.kind === 'unsupported' ? 'error' : 'error', code: item.code, message: item.message });
|
|
54
|
+
if (!parsed.workspaceArtifactPath || !parsed.protectedPayloadDescriptorPath) findings.push({ severity: 'error', code: 'secure-transport.envelope.binding-links-invalid', message: 'Transport Envelope Workspace Artifact and Protected Payload links are required.' });
|
|
55
|
+
return deepFreeze({ status: findings.length ? (crypto.state === 'unsupported' ? 'unsupported' : 'invalid') : 'qualified', parsed, findings });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseSlots(text = '') {
|
|
59
|
+
const out = [];
|
|
60
|
+
const re = /^-\s+([^\n]+)\n((?:\s{2}-\s+[^\n]+\n?)*)/gm;
|
|
61
|
+
for (const match of String(text || '').matchAll(re)) {
|
|
62
|
+
const section = String(match[2] || '');
|
|
63
|
+
out.push(deepFreeze({
|
|
64
|
+
slotId: field(section, 'Slot Id'), slotKind: field(section, 'Slot Kind'), kdfAlgorithm: field(section, 'KDF Algorithm'), kdfSaltEncoding: field(section, 'KDF Salt Encoding'), kdfSalt: field(section, 'KDF Salt'), kdfParameters: jsonObjectField(section, 'KDF Parameters'), keyWrapAlgorithm: field(section, 'Key Wrap Algorithm'), keyWrapParameters: jsonObjectField(section, 'Key Wrap Parameters'), wrappedContentKeyEncoding: field(section, 'Wrapped Content Key Encoding'), wrappedContentKey: field(section, 'Wrapped Content Key'), slotVerificationRule: field(section, 'Slot Verification Rule'), ...(field(section, 'Recipient Hint') ? { recipientHint: field(section, 'Recipient Hint') } : {})
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
return Object.freeze(out);
|
|
68
|
+
}
|
|
69
|
+
function jsonObjectField(section, name) { const raw = unquote(field(section, name)); try { const value = JSON.parse(raw); return value && typeof value === 'object' && !Array.isArray(value) ? value : null; } catch { return null; } }
|
|
70
|
+
function numberField(section, name) { const raw = field(section, name); return /^\d+$/.test(raw) ? Number(raw) : null; }
|
|
71
|
+
function renderParentEnvelope(parent = null) { if (!parent) return ''; const path=String(parent.path||'').trim(), label=String(parent.label||path||'Parent'), schemaId=String(parent.schemaId||'').trim(), schemaTarget=String(parent.schemaTarget||'').trim(), createdAt=normalizeCreatedAt(parent.createdAt||''); if(!path||!schemaId||!schemaTarget||!String(parent.selfDigest||'').trim()) throw new Error('secure-transport.parent-authority-incomplete'); return `- Parent\n - Parent Schema: [${schemaId}](${schemaTarget})\n - Created At: ${createdAt}\n - Trace: [${label}](${path})\n - Origin:\n - [relative](${path})\n`; }
|
|
72
|
+
function renderParentIntegrity(parent = null) { if(!parent) return ''; const path=String(parent.path||'').trim(), label=String(parent.label||path||'Parent'), digest=String(parent.selfDigest||'').trim(); if(!path||!digest) throw new Error('secure-transport.parent-integrity-incomplete'); return `- [sha256-base64url-c14n-v2](${C14N_V2_VALIDATOR_TARGET})\n - Towards: [${label}](${path})\n - Value: ${digest}\n\n`; }
|
|
73
|
+
function normalizeCreatedAt(value=''){const text=String(value||'').trim(); return text ? text.replace('T',' ').replace(/\.\d{3}Z$/,'').replace(/Z$/,'').slice(0,19) : '1970-01-01 00:00:00';}
|
|
74
|
+
function stableJson(value) { return JSON.stringify(sortJson(value)); }
|
|
75
|
+
function sortJson(value) { if (Array.isArray(value)) return value.map(sortJson); if (!value || typeof value !== 'object') return value; return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])])); }
|
|
76
|
+
function deepFreeze(value){if(!value||typeof value!=='object'||Object.isFrozen(value))return value;if(ArrayBuffer.isView(value)||value instanceof ArrayBuffer)return value;for(const child of Object.values(value))deepFreeze(child);return Object.freeze(value);}
|
|
@@ -50,3 +50,7 @@ export * from './handoff/manufacture.js';
|
|
|
50
50
|
export * from './handoff/carrierProfile.js';
|
|
51
51
|
export * from './handoff/toolingBootstrap.js';
|
|
52
52
|
export * from './handoff/coldStartQualification.js';
|
|
53
|
+
export * from './handoff/transportEnvelopeV1.js';
|
|
54
|
+
export * from './handoff/recipientV2.packageV1.js';
|
|
55
|
+
|
|
56
|
+
export * from './comparison/sourceFrontierComparison.js';
|
|
@@ -38,10 +38,18 @@ import { describePortableColdStartIngress, groundPortableColdConsumer, projectPo
|
|
|
38
38
|
import { projectPortableOperatingOverview } from './overview/operatingOverview.js';
|
|
39
39
|
import { projectPortableGroundingReadiness } from './grounding/grounding.readiness.js';
|
|
40
40
|
import { createPortablePackageOperationEntries } from './operation.catalog.package.js';
|
|
41
|
+
import { compareOrReconcilePortableSourceFrontiers } from './comparison/sourceFrontierComparison.js';
|
|
41
42
|
|
|
42
43
|
export const PORTABLE_OPERATION_CATALOG_SCHEMA_ID = 'tiinex.portable.operation.catalog.v1';
|
|
43
44
|
|
|
44
45
|
export const portableOperationCatalog = Object.freeze({
|
|
46
|
+
'compare-source-frontiers': operation({
|
|
47
|
+
name: 'compare-source-frontiers',
|
|
48
|
+
description: 'Compare normalized exact Workspace source frontiers two-way or reconcile base/incoming/current three-way without merge, semantic inference, remote acquisition, or source mutation.',
|
|
49
|
+
safety: 'read-only',
|
|
50
|
+
inputSchema: 'tiinex.portable.source-frontier-comparison.request.v1',
|
|
51
|
+
handler: (input = {}) => wrapPortableResult('compare-source-frontiers', compareOrReconcilePortableSourceFrontiers(input))
|
|
52
|
+
}),
|
|
45
53
|
'prepare-task': operation({
|
|
46
54
|
name: 'prepare-task',
|
|
47
55
|
description: 'Orchestrate host discovery, schema/provider resolution, schema guides, artifact planning, draft validation, lineage search, or asset analysis into one explicit next-action response.',
|
|
@@ -11,7 +11,6 @@ import { projectPortableEditorAssistance } from './editor/editor.assistance.js';
|
|
|
11
11
|
import { projectQualifiedHandoffLeaves } from './handoff/handoffLeafProjection.js';
|
|
12
12
|
import { projectPortableAuthoringParent } from './editor/authoring.parent.js';
|
|
13
13
|
import { projectQualifiedWorkspacePackageSources } from './handoff/workspacePackageSources.js';
|
|
14
|
-
import { compareSourceFrontiers } from './handoff/sourceFrontierComparison.js';
|
|
15
14
|
import { projectPortableHandoffAuthoringPlan } from './handoff/handoffAuthoringPlan.js';
|
|
16
15
|
import { projectQualifiedHandoffEndpoints } from './handoff/handoffEndpointProjection.js';
|
|
17
16
|
import { projectPortableOperatorContext } from './handoff/operatorContextProjection.js';
|
|
@@ -96,13 +95,6 @@ export function createPortablePackageOperationEntries({ operation, wrapPortableR
|
|
|
96
95
|
inputSchema: 'tiinex.portable.input.v1',
|
|
97
96
|
handler: (input = {}) => wrapPortableResult('project-workspace-package-sources', projectQualifiedWorkspacePackageSources(input))
|
|
98
97
|
}),
|
|
99
|
-
'compare-source-frontiers': operation({
|
|
100
|
-
name: 'compare-source-frontiers',
|
|
101
|
-
description: 'Compare one local Workspace root against one carried workspace snapshot bound by a recipient-facing Handoff package.',
|
|
102
|
-
safety: 'read-only',
|
|
103
|
-
inputSchema: 'tiinex.portable.input.v1',
|
|
104
|
-
handler: async (input = {}) => wrapPortableResult('compare-source-frontiers', await compareSourceFrontiers(input))
|
|
105
|
-
}),
|
|
106
98
|
'project-handoff-authoring-plan': operation({
|
|
107
99
|
name: 'project-handoff-authoring-plan',
|
|
108
100
|
description: 'Project the shared root or continuation path allocation for native Handoff authoring from exact local material without creating an artifact.',
|