arcane-os 0.1.0-dev.5
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/CHANGELOG.md +154 -0
- package/COMMERCIAL-LICENSE.md +13 -0
- package/LICENSE +661 -0
- package/NOTICE +70 -0
- package/README.md +448 -0
- package/bin/arcane-test.mjs +741 -0
- package/bin/arcane.mjs +5 -0
- package/docs/architecture.md +234 -0
- package/docs/compatibility.md +36 -0
- package/docs/event-manager.md +166 -0
- package/docs/platform-targets.md +108 -0
- package/docs/publishing.md +203 -0
- package/docs/reference/README.md +117 -0
- package/docs/reference/arcane-ollama.md +288 -0
- package/docs/reference/availability-and-normalization.md +123 -0
- package/docs/reference/behavioral-testing.md +86 -0
- package/docs/reference/cli.md +569 -0
- package/docs/reference/core/README.md +62 -0
- package/docs/reference/core/arcane-ai-contracts.md +873 -0
- package/docs/reference/core/arcane-api.md +601 -0
- package/docs/reference/core/arcane-entities.md +65 -0
- package/docs/reference/core/arcane-events.md +134 -0
- package/docs/reference/core/ollama-module.md +181 -0
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
- package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
- package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
- package/docs/reference/event-manager.md +957 -0
- package/docs/reference/inventory/package-api.json +2632 -0
- package/docs/reference/inventory/runtime-components.json +934 -0
- package/docs/reference/inventory/runtime-entities.json +26 -0
- package/docs/reference/inventory/runtime-modules.json +1249 -0
- package/docs/reference/protocols.md +242 -0
- package/docs/reference/runtime-components.md +1098 -0
- package/docs/reference/runtime-entities.md +303 -0
- package/docs/reference/runtime-modules.md +2010 -0
- package/docs/reference/sdk-api.md +4901 -0
- package/docs/roadmap.md +79 -0
- package/docs/work-amplification.md +124 -0
- package/node_modules/event-pubsub/CHANGELOG.md +55 -0
- package/node_modules/event-pubsub/MIGRATION.md +70 -0
- package/node_modules/event-pubsub/README.md +363 -0
- package/node_modules/event-pubsub/SECURITY.md +37 -0
- package/node_modules/event-pubsub/index.js +141 -0
- package/node_modules/event-pubsub/licence +21 -0
- package/node_modules/event-pubsub/package.json +59 -0
- package/node_modules/strong-type/README.md +408 -0
- package/node_modules/strong-type/assets/strong-type-header.png +0 -0
- package/node_modules/strong-type/index.js +1151 -0
- package/node_modules/strong-type/licence +21 -0
- package/node_modules/strong-type/node.js +125 -0
- package/node_modules/strong-type/package.json +61 -0
- package/package.json +95 -0
- package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
- package/runtime/arcane/components/app-bar.html +468 -0
- package/runtime/arcane/components/assistant-panel.html +715 -0
- package/runtime/arcane/components/calculator.html +8 -0
- package/runtime/arcane/components/chart.html +655 -0
- package/runtime/arcane/components/chat.html +1225 -0
- package/runtime/arcane/components/conversation-view.html +13 -0
- package/runtime/arcane/components/dashboard-config.html +341 -0
- package/runtime/arcane/components/data-maintenance.html +112 -0
- package/runtime/arcane/components/data-view.html +92 -0
- package/runtime/arcane/components/directory-picker.html +197 -0
- package/runtime/arcane/components/document-inspector.html +252 -0
- package/runtime/arcane/components/file-drop.html +264 -0
- package/runtime/arcane/components/file-inspector.html +293 -0
- package/runtime/arcane/components/file-manager.html +1715 -0
- package/runtime/arcane/components/header.html +142 -0
- package/runtime/arcane/components/integration-settings.html +14 -0
- package/runtime/arcane/components/local-ai-status.html +360 -0
- package/runtime/arcane/components/markdown-document.html +1048 -0
- package/runtime/arcane/components/markdown-editor.html +360 -0
- package/runtime/arcane/components/media-embed.html +8 -0
- package/runtime/arcane/components/modal.html +402 -0
- package/runtime/arcane/components/output-panel.html +259 -0
- package/runtime/arcane/components/preferences-form.html +135 -0
- package/runtime/arcane/components/record-timeline.html +105 -0
- package/runtime/arcane/components/relationship-board.html +116 -0
- package/runtime/arcane/components/screen-capture.html +8 -0
- package/runtime/arcane/components/source-code-viewer.html +441 -0
- package/runtime/arcane/components/source-explanation.html +124 -0
- package/runtime/arcane/components/speech.html +365 -0
- package/runtime/arcane/components/summary-strip.html +177 -0
- package/runtime/arcane/components/table.html +77 -0
- package/runtime/arcane/components/task-progress.html +282 -0
- package/runtime/arcane/components/terminal-workspace.html +65 -0
- package/runtime/arcane/components/theme-editor.html +41 -0
- package/runtime/arcane/components/theme-switcher.html +46 -0
- package/runtime/arcane/components/unified-inbox.html +20 -0
- package/runtime/arcane/components/voice-transcription.html +476 -0
- package/runtime/arcane/components/weather-widget.html +8 -0
- package/runtime/arcane/components/web-navigator.html +239 -0
- package/runtime/arcane/css/communications.css +1 -0
- package/runtime/arcane/css/dashboard-config.css +45 -0
- package/runtime/arcane/css/document-site.css +981 -0
- package/runtime/arcane/css/layout.css +438 -0
- package/runtime/arcane/css/primitives.css +321 -0
- package/runtime/arcane/css/theme.css +112 -0
- package/runtime/arcane/css/utility-workspace.css +1 -0
- package/runtime/arcane/entities/ApiModelRecord.js +20 -0
- package/runtime/arcane/entities/Calculation.js +13 -0
- package/runtime/arcane/entities/Chat.js +581 -0
- package/runtime/arcane/entities/CommunicationMessage.js +29 -0
- package/runtime/arcane/entities/CommunicationThread.js +21 -0
- package/runtime/arcane/entities/Document.js +10 -0
- package/runtime/arcane/entities/File.js +143 -0
- package/runtime/arcane/entities/Image.js +135 -0
- package/runtime/arcane/entities/IntentEnvelope.js +834 -0
- package/runtime/arcane/entities/Preference.js +83 -0
- package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
- package/runtime/arcane/entities/TerminalSession.js +46 -0
- package/runtime/arcane/entities/Theme.js +107 -0
- package/runtime/arcane/entities/User.js +1046 -0
- package/runtime/arcane/entities/Weather.js +23 -0
- package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
- package/runtime/arcane/img/arrow-left.png +0 -0
- package/runtime/arcane/img/arrow-right.png +0 -0
- package/runtime/arcane/img/doc.svg +5 -0
- package/runtime/arcane/img/folder.svg +4 -0
- package/runtime/arcane/img/image.svg +5 -0
- package/runtime/arcane/img/refresh.png +0 -0
- package/runtime/arcane/img/send.svg +9 -0
- package/runtime/arcane/img/trash.svg +5 -0
- package/runtime/arcane/img/upload.svg +5 -0
- package/runtime/arcane/modules/AI.js +2048 -0
- package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
- package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
- package/runtime/arcane/modules/AIResponseLength.js +42 -0
- package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
- package/runtime/arcane/modules/AnsiText.js +53 -0
- package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
- package/runtime/arcane/modules/AppDataScope.js +245 -0
- package/runtime/arcane/modules/AppearancePreferences.js +28 -0
- package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
- package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
- package/runtime/arcane/modules/AsyncBoundary.js +161 -0
- package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
- package/runtime/arcane/modules/CalculatorEngine.js +20 -0
- package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
- package/runtime/arcane/modules/ChartLibrary.js +35 -0
- package/runtime/arcane/modules/ChatRecords.js +13 -0
- package/runtime/arcane/modules/CommunicationAppController.js +43 -0
- package/runtime/arcane/modules/CommunicationHub.js +16 -0
- package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
- package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
- package/runtime/arcane/modules/ComponentContracts.js +586 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
- package/runtime/arcane/modules/ConversationActionItems.js +488 -0
- package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
- package/runtime/arcane/modules/ConversationTimebox.js +527 -0
- package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
- package/runtime/arcane/modules/DBLS.js +171 -0
- package/runtime/arcane/modules/DBOPFS.js +1154 -0
- package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
- package/runtime/arcane/modules/DataMaintenance.js +91 -0
- package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
- package/runtime/arcane/modules/DirectoryPicker.js +109 -0
- package/runtime/arcane/modules/DocumentNavigation.js +223 -0
- package/runtime/arcane/modules/Errors.js +1025 -0
- package/runtime/arcane/modules/GifEncoder.js +29 -0
- package/runtime/arcane/modules/HTMLImport.js +114 -0
- package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
- package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
- package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
- package/runtime/arcane/modules/MD.js +111 -0
- package/runtime/arcane/modules/Mail.js +352 -0
- package/runtime/arcane/modules/MailTransport.mjs +180 -0
- package/runtime/arcane/modules/Marked.min.js +71 -0
- package/runtime/arcane/modules/MemoryRecords.js +44 -0
- package/runtime/arcane/modules/MessageAdvisory.js +38 -0
- package/runtime/arcane/modules/ModelDefinition.js +189 -0
- package/runtime/arcane/modules/Ollama.js +74 -0
- package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
- package/runtime/arcane/modules/OllamaSettings.js +24 -0
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
- package/runtime/arcane/modules/PreferenceStore.js +109 -0
- package/runtime/arcane/modules/QRCode.min.js +1 -0
- package/runtime/arcane/modules/Questionnaire.js +61 -0
- package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
- package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
- package/runtime/arcane/modules/RecordReviewStore.js +94 -0
- package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
- package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
- package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
- package/runtime/arcane/modules/ScreenCapture.js +20 -0
- package/runtime/arcane/modules/SpeechPlayback.js +581 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
- package/runtime/arcane/modules/SystemAppearance.js +20 -0
- package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
- package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
- package/runtime/arcane/modules/TerminalClient.js +52 -0
- package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
- package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
- package/runtime/arcane/modules/ThemeManager.js +131 -0
- package/runtime/arcane/modules/TimeGuard.js +149 -0
- package/runtime/arcane/modules/ToolCallRouter.js +83 -0
- package/runtime/arcane/modules/WaitForComponent.js +102 -0
- package/runtime/arcane/modules/YouTubeMedia.js +16 -0
- package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
- package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
- package/runtime/arcane/modules/uPlot.min.css +1 -0
- package/runtime/arcane/security/arcane-network-policy.json +6 -0
- package/runtime/strong-type/index.js +352 -0
- package/runtime/strong-type/licence +21 -0
- package/runtime/strong-type/package.json +45 -0
- package/schemas/arcane-app-bundle.schema.json +125 -0
- package/schemas/arcane-app.schema.json +329 -0
- package/schemas/arcane-lock.schema.json +86 -0
- package/schemas/arcane-package.schema.json +224 -0
- package/schemas/cli-event.schema.json +122 -0
- package/schemas/event-stack.schema.json +152 -0
- package/schemas/native-build-plan.schema.json +176 -0
- package/schemas/target-adapter.schema.json +117 -0
- package/src/app-descriptor.mjs +500 -0
- package/src/cli/main.mjs +561 -0
- package/src/constants.mjs +31 -0
- package/src/dev-server.mjs +718 -0
- package/src/doctor.mjs +315 -0
- package/src/dom-event-instrumentation.mjs +594 -0
- package/src/errors.mjs +75 -0
- package/src/event-manager.mjs +1342 -0
- package/src/event-queue.mjs +138 -0
- package/src/events.mjs +219 -0
- package/src/index.mjs +177 -0
- package/src/integrated-provider-loader.mjs +432 -0
- package/src/native-plan.mjs +698 -0
- package/src/native-provider-loader.mjs +1126 -0
- package/src/packager/core.mjs +2691 -0
- package/src/process.mjs +353 -0
- package/src/release-bundle.mjs +2523 -0
- package/src/repository.mjs +90 -0
- package/src/runtime.mjs +452 -0
- package/src/scaffold.mjs +380 -0
- package/src/targets/index.mjs +436 -0
- package/src/templates/assets/app-icon.png +0 -0
- package/src/templates/workspace-template.mjs +388 -0
- package/src/testing-loader.mjs +9 -0
- package/src/testing.mjs +427 -0
- package/src/toolchain.mjs +1335 -0
- package/src/update-check.mjs +307 -0
- package/src/workspace.mjs +449 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {runProcess} from './process.mjs';
|
|
2
|
+
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
3
|
+
|
|
4
|
+
async function git(args,{run=runProcess,...options}){
|
|
5
|
+
return run('git',args,options);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function repositoryStatus({
|
|
9
|
+
workspaceRoot=process.cwd(),
|
|
10
|
+
signal,
|
|
11
|
+
onEvent,
|
|
12
|
+
run=runProcess
|
|
13
|
+
}={}){
|
|
14
|
+
throwIfAborted(signal);
|
|
15
|
+
// These commands intentionally share one public event callback. Run them in
|
|
16
|
+
// a fixed order so each process owns and drains its events before the next
|
|
17
|
+
// producer begins.
|
|
18
|
+
const root=await git(['rev-parse','--show-toplevel'],{cwd:workspaceRoot,signal,onEvent,run});
|
|
19
|
+
const branch=await git(['branch','--show-current'],{cwd:workspaceRoot,signal,onEvent,run});
|
|
20
|
+
const status=await git(['status','--short','--branch'],{cwd:workspaceRoot,signal,onEvent,run});
|
|
21
|
+
return {
|
|
22
|
+
repositoryRoot:root.stdout.trim(),
|
|
23
|
+
branch:branch.stdout.trim()||null,
|
|
24
|
+
clean:status.stdout.split(/\r?\n/u).filter(Boolean).every(line=>line.startsWith('##')),
|
|
25
|
+
status:status.stdout.trim()
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function repositoryPull({
|
|
30
|
+
workspaceRoot=process.cwd(),
|
|
31
|
+
signal,
|
|
32
|
+
onEvent,
|
|
33
|
+
run=runProcess
|
|
34
|
+
}={}){
|
|
35
|
+
throwIfAborted(signal);
|
|
36
|
+
const before=await repositoryStatus({workspaceRoot,signal,onEvent,run});
|
|
37
|
+
if(!before.clean){
|
|
38
|
+
throw new ArcaneError(
|
|
39
|
+
ERROR_CODES.policyDenied,
|
|
40
|
+
'Refusing to pull into a repository with uncommitted changes.',
|
|
41
|
+
{details:before}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const result=await git(['pull','--ff-only'],{cwd:workspaceRoot,signal,onEvent,run});
|
|
45
|
+
return {
|
|
46
|
+
action:'pull',
|
|
47
|
+
repositoryRoot:before.repositoryRoot,
|
|
48
|
+
branch:before.branch,
|
|
49
|
+
output:result.stdout.trim()
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function repositoryPush({
|
|
54
|
+
workspaceRoot=process.cwd(),
|
|
55
|
+
signal,
|
|
56
|
+
onEvent,
|
|
57
|
+
run=runProcess
|
|
58
|
+
}={}){
|
|
59
|
+
throwIfAborted(signal);
|
|
60
|
+
const before=await repositoryStatus({workspaceRoot,signal,onEvent,run});
|
|
61
|
+
if(!before.branch){
|
|
62
|
+
throw new ArcaneError(
|
|
63
|
+
ERROR_CODES.policyDenied,
|
|
64
|
+
'Refusing to push from a detached HEAD.'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const result=await git(['push'],{cwd:workspaceRoot,signal,onEvent,run});
|
|
68
|
+
return {
|
|
69
|
+
action:'push',
|
|
70
|
+
repositoryRoot:before.repositoryRoot,
|
|
71
|
+
branch:before.branch,
|
|
72
|
+
output:(result.stdout||result.stderr).trim()
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runRepositoryAction(action,options={}){
|
|
77
|
+
if(action==='status'){
|
|
78
|
+
return repositoryStatus(options);
|
|
79
|
+
}
|
|
80
|
+
if(action==='pull'){
|
|
81
|
+
return repositoryPull(options);
|
|
82
|
+
}
|
|
83
|
+
if(action==='push'){
|
|
84
|
+
return repositoryPush(options);
|
|
85
|
+
}
|
|
86
|
+
throw new ArcaneError(
|
|
87
|
+
ERROR_CODES.usage,
|
|
88
|
+
`Unknown repository action: ${String(action)}. Expected status, pull, or push.`
|
|
89
|
+
);
|
|
90
|
+
}
|
package/src/runtime.mjs
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import {createHash} from 'node:crypto';
|
|
2
|
+
import {constants as FS_CONSTANTS} from 'node:fs';
|
|
3
|
+
import {lstat,open,readdir,realpath} from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {SDK_VERSION} from './constants.mjs';
|
|
7
|
+
|
|
8
|
+
export const RUNTIME_MANIFEST_NAME='ARCANE_RUNTIME_RELEASE.json';
|
|
9
|
+
const SHA256_PATTERN=/^[a-f0-9]{64}$/;
|
|
10
|
+
const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
|
|
11
|
+
const MAX_VERIFIED_RUNTIME_FILE_BYTES=64*1024*1024;
|
|
12
|
+
const sdkRoot=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
13
|
+
const issuedReceipts=new WeakSet();
|
|
14
|
+
|
|
15
|
+
function fail(message,code='ARCANE_INTEGRITY_FAILED'){
|
|
16
|
+
const error=new Error(message);
|
|
17
|
+
error.code=code;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function throwIfAborted(signal){
|
|
22
|
+
if(!signal?.aborted)return;
|
|
23
|
+
const error=signal.reason instanceof Error?signal.reason:new Error('Operation cancelled.');
|
|
24
|
+
error.code=error.code||'ARCANE_CANCELLED';
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function emit(onEvent,event){
|
|
29
|
+
if(typeof onEvent==='function')await onEvent(Object.freeze(event));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function compareText(left,right){
|
|
33
|
+
const a=String(left);
|
|
34
|
+
const b=String(right);
|
|
35
|
+
return a<b?-1:a>b?1:0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function immutableFileInventory(files){
|
|
39
|
+
return Object.freeze(files.map(file=>Object.freeze({...file})));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeInventoryPath(value){
|
|
43
|
+
if(typeof value!=='string'||!value||value.includes('\\')||value.includes('\0')
|
|
44
|
+
||path.posix.isAbsolute(value)||path.posix.normalize(value)!==value
|
|
45
|
+
||value==='.'||value.startsWith('../')||value.includes('/../')){
|
|
46
|
+
fail(`Runtime manifest contains an unsafe path: ${String(value)}.`);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateRelease(value){
|
|
52
|
+
if(!value||typeof value!=='object'||Array.isArray(value))fail('Runtime manifest must be a JSON object.');
|
|
53
|
+
const releaseKeys=['builder','contentSha256','fileCount','files','schemaVersion','sdkVersion','source','totalBytes'];
|
|
54
|
+
if(Object.keys(value).sort(compareText).join('\0')!==releaseKeys.sort(compareText).join('\0')){
|
|
55
|
+
fail('Runtime manifest contains missing or unsupported fields.');
|
|
56
|
+
}
|
|
57
|
+
if(value.schemaVersion!==1||value.builder!=='arcane-sdk-runtime-v1'){
|
|
58
|
+
fail('Runtime manifest uses an unsupported schema or builder.');
|
|
59
|
+
}
|
|
60
|
+
if(value.sdkVersion!==SDK_VERSION)fail('Runtime manifest sdkVersion is incompatible with this SDK.');
|
|
61
|
+
if(!value.source||typeof value.source!=='object'
|
|
62
|
+
||Object.keys(value.source).sort(compareText).join('\0')!=='bundleVersion\0commit\0protocol\0repository'
|
|
63
|
+
||value.source.repository!=='https://github.com/TheWizardNexus/ARCANE-OS.git'
|
|
64
|
+
||typeof value.source.commit!=='string'||!/^[a-f0-9]{40}$/.test(value.source.commit)
|
|
65
|
+
||value.source.bundleVersion!=='0.8.12'||value.source.protocol!=='arcane/1'){
|
|
66
|
+
fail('Runtime manifest source identity is invalid.');
|
|
67
|
+
}
|
|
68
|
+
if(!Array.isArray(value.files)||!Number.isSafeInteger(value.fileCount)
|
|
69
|
+
||value.fileCount!==value.files.length||!Number.isSafeInteger(value.totalBytes)
|
|
70
|
+
||value.totalBytes<0||!SHA256_PATTERN.test(value.contentSha256)){
|
|
71
|
+
fail('Runtime manifest inventory summary is invalid.');
|
|
72
|
+
}
|
|
73
|
+
let previous='';
|
|
74
|
+
let totalBytes=0;
|
|
75
|
+
const seen=new Set();
|
|
76
|
+
const files=value.files.map((entry,index)=>{
|
|
77
|
+
if(!entry||typeof entry!=='object'||Array.isArray(entry)){
|
|
78
|
+
fail(`Runtime manifest file ${index} is invalid.`);
|
|
79
|
+
}
|
|
80
|
+
if(Object.keys(entry).sort(compareText).join('\0')!=='bytes\0path\0sha256'){
|
|
81
|
+
fail(`Runtime manifest file ${index} contains missing or unsupported fields.`);
|
|
82
|
+
}
|
|
83
|
+
const relative=safeInventoryPath(entry.path);
|
|
84
|
+
if(relative===RUNTIME_MANIFEST_NAME||seen.has(relative)
|
|
85
|
+
||(previous&&compareText(previous,relative)>=0)){
|
|
86
|
+
fail(`Runtime manifest inventory is not unique and sorted at ${relative}.`);
|
|
87
|
+
}
|
|
88
|
+
if(!Number.isSafeInteger(entry.bytes)||entry.bytes<0||!SHA256_PATTERN.test(entry.sha256)){
|
|
89
|
+
fail(`Runtime manifest metadata is invalid for ${relative}.`);
|
|
90
|
+
}
|
|
91
|
+
seen.add(relative);
|
|
92
|
+
previous=relative;
|
|
93
|
+
totalBytes+=entry.bytes;
|
|
94
|
+
if(!Number.isSafeInteger(totalBytes))fail('Runtime manifest total byte count is too large.');
|
|
95
|
+
return {path:relative,bytes:entry.bytes,sha256:entry.sha256};
|
|
96
|
+
});
|
|
97
|
+
if(totalBytes!==value.totalBytes)fail('Runtime manifest totalBytes does not match its inventory.');
|
|
98
|
+
const aggregate=createHash('sha256').update(JSON.stringify(files)).digest('hex');
|
|
99
|
+
if(aggregate!==value.contentSha256)fail('Runtime manifest contentSha256 does not match its inventory.');
|
|
100
|
+
return {...value,files};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readRelease(runtimeRoot){
|
|
104
|
+
const manifestPath=path.join(runtimeRoot,RUNTIME_MANIFEST_NAME);
|
|
105
|
+
let bytes;
|
|
106
|
+
let manifestIdentity;
|
|
107
|
+
try{
|
|
108
|
+
const info=await lstat(manifestPath);
|
|
109
|
+
if(info.isSymbolicLink()||!info.isFile())fail('SDK runtime manifest must be a real file.');
|
|
110
|
+
const handle=await open(manifestPath,'r');
|
|
111
|
+
try{
|
|
112
|
+
const before=await handle.stat({bigint:true});
|
|
113
|
+
bytes=await handle.readFile();
|
|
114
|
+
const after=await handle.stat({bigint:true});
|
|
115
|
+
if(!sameIdentity(before,after))fail('SDK runtime manifest changed while it was being read.');
|
|
116
|
+
manifestIdentity=fileIdentity(after);
|
|
117
|
+
}finally{
|
|
118
|
+
await handle.close();
|
|
119
|
+
}
|
|
120
|
+
}catch(error){
|
|
121
|
+
if(error?.code==='ENOENT')fail(`SDK runtime manifest is missing: ${manifestPath}.`);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
let value;
|
|
125
|
+
try{
|
|
126
|
+
value=JSON.parse(bytes.toString('utf8'));
|
|
127
|
+
}catch(error){
|
|
128
|
+
fail(`SDK runtime manifest is not valid JSON: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
release:validateRelease(value),
|
|
132
|
+
manifestPath,
|
|
133
|
+
manifestSha256:createHash('sha256').update(bytes).digest('hex'),
|
|
134
|
+
manifestIdentity
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function listRuntimeFiles(root,{signal}={}){
|
|
139
|
+
const found=[];
|
|
140
|
+
async function visit(directory,relativeRoot=''){
|
|
141
|
+
throwIfAborted(signal);
|
|
142
|
+
const entries=await readdir(directory,{withFileTypes:true});
|
|
143
|
+
entries.sort((left,right)=>compareText(left.name,right.name));
|
|
144
|
+
for(const entry of entries){
|
|
145
|
+
throwIfAborted(signal);
|
|
146
|
+
const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
|
|
147
|
+
if(relative===RUNTIME_MANIFEST_NAME)continue;
|
|
148
|
+
const absolute=path.join(directory,entry.name);
|
|
149
|
+
const details=await lstat(absolute);
|
|
150
|
+
if(details.isSymbolicLink())fail(`SDK runtime contains a symbolic link or junction: ${relative}.`);
|
|
151
|
+
if(details.isDirectory())await visit(absolute,relative);
|
|
152
|
+
else if(details.isFile())found.push(relative);
|
|
153
|
+
else fail(`SDK runtime contains a non-file entry: ${relative}.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
await visit(root);
|
|
157
|
+
return found.sort(compareText);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sameIdentity(before,after){
|
|
161
|
+
return before.dev===after.dev&&before.ino===after.ino&&before.size===after.size
|
|
162
|
+
&&before.mtimeNs===after.mtimeNs&&before.ctimeNs===after.ctimeNs
|
|
163
|
+
&&before.nlink===after.nlink;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function fileIdentity(info){
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
device:String(info.dev),
|
|
169
|
+
inode:String(info.ino),
|
|
170
|
+
bytes:Number(info.size),
|
|
171
|
+
modifiedNanoseconds:String(info.mtimeNs),
|
|
172
|
+
changedNanoseconds:String(info.ctimeNs),
|
|
173
|
+
links:String(info.nlink)
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function identityMatches(info,identity){
|
|
178
|
+
return String(info.dev)===identity.device
|
|
179
|
+
&&String(info.ino)===identity.inode
|
|
180
|
+
&&Number(info.size)===identity.bytes
|
|
181
|
+
&&String(info.mtimeNs)===identity.modifiedNanoseconds
|
|
182
|
+
&&String(info.ctimeNs)===identity.changedNanoseconds
|
|
183
|
+
&&String(info.nlink)===identity.links;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function assertRuntimeState({
|
|
187
|
+
canonicalRoot,
|
|
188
|
+
rootIdentity,
|
|
189
|
+
manifestPath,
|
|
190
|
+
manifestIdentity,
|
|
191
|
+
identities,
|
|
192
|
+
signal
|
|
193
|
+
}){
|
|
194
|
+
throwIfAborted(signal);
|
|
195
|
+
const rootBefore=await lstat(canonicalRoot,{bigint:true});
|
|
196
|
+
if(rootBefore.isSymbolicLink()||!rootBefore.isDirectory()
|
|
197
|
+
||!identityMatches(rootBefore,rootIdentity)){
|
|
198
|
+
fail('SDK runtime root changed after verification.');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const actualPaths=await listRuntimeFiles(canonicalRoot,{signal});
|
|
202
|
+
const expectedPaths=identities.map(identity=>identity.path);
|
|
203
|
+
if(JSON.stringify(actualPaths)!==JSON.stringify(expectedPaths)){
|
|
204
|
+
fail('SDK runtime file inventory changed after verification.');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const manifestInfo=await lstat(manifestPath,{bigint:true});
|
|
208
|
+
if(manifestInfo.isSymbolicLink()||!manifestInfo.isFile()
|
|
209
|
+
||!identityMatches(manifestInfo,manifestIdentity)){
|
|
210
|
+
fail('SDK runtime manifest changed after verification.');
|
|
211
|
+
}
|
|
212
|
+
for(const identity of identities){
|
|
213
|
+
throwIfAborted(signal);
|
|
214
|
+
const filePath=path.resolve(canonicalRoot,...identity.path.split('/'));
|
|
215
|
+
const relative=path.relative(canonicalRoot,filePath);
|
|
216
|
+
if(relative.startsWith('..')||path.isAbsolute(relative)){
|
|
217
|
+
fail(`Runtime receipt path escapes its root: ${identity.path}.`);
|
|
218
|
+
}
|
|
219
|
+
const info=await lstat(filePath,{bigint:true});
|
|
220
|
+
if(info.isSymbolicLink()||!info.isFile()||!identityMatches(info,identity)){
|
|
221
|
+
fail(`SDK runtime file changed after verification: ${identity.path}.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const rootAfter=await lstat(canonicalRoot,{bigint:true});
|
|
226
|
+
if(rootAfter.isSymbolicLink()||!rootAfter.isDirectory()
|
|
227
|
+
||!identityMatches(rootAfter,rootIdentity)
|
|
228
|
+
||!sameIdentity(rootBefore,rootAfter)){
|
|
229
|
+
fail('SDK runtime root changed while its receipt was authenticated.');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function hashExactFile(filePath,expectedBytes,signal){
|
|
234
|
+
throwIfAborted(signal);
|
|
235
|
+
const handle=await open(filePath,'r');
|
|
236
|
+
const hash=createHash('sha256');
|
|
237
|
+
const buffer=Buffer.allocUnsafe(1024*1024);
|
|
238
|
+
try{
|
|
239
|
+
const before=await handle.stat({bigint:true});
|
|
240
|
+
if(!before.isFile()||before.size!==BigInt(expectedBytes)){
|
|
241
|
+
fail(`SDK runtime file size changed: ${filePath}.`);
|
|
242
|
+
}
|
|
243
|
+
while(true){
|
|
244
|
+
throwIfAborted(signal);
|
|
245
|
+
const {bytesRead}=await handle.read(buffer,0,buffer.length,null);
|
|
246
|
+
if(bytesRead===0)break;
|
|
247
|
+
hash.update(buffer.subarray(0,bytesRead));
|
|
248
|
+
}
|
|
249
|
+
const after=await handle.stat({bigint:true});
|
|
250
|
+
if(!sameIdentity(before,after))fail(`SDK runtime file changed while it was being verified: ${filePath}.`);
|
|
251
|
+
return {sha256:hash.digest('hex'),identity:fileIdentity(after)};
|
|
252
|
+
}finally{
|
|
253
|
+
await handle.close();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function getSdkRoot(){
|
|
258
|
+
return sdkRoot;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function loadRuntimeRelease({runtimeRoot=path.join(sdkRoot,'runtime')}={}){
|
|
262
|
+
const resolved=path.resolve(runtimeRoot);
|
|
263
|
+
return (await readRelease(resolved)).release;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export async function authenticateRuntimeReceipt(receipt,{
|
|
267
|
+
runtimeRoot=path.join(sdkRoot,'runtime'),
|
|
268
|
+
signal
|
|
269
|
+
}={}){
|
|
270
|
+
if(!receipt||!issuedReceipts.has(receipt)){
|
|
271
|
+
fail('Runtime verification receipt was not issued by this SDK process.');
|
|
272
|
+
}
|
|
273
|
+
throwIfAborted(signal);
|
|
274
|
+
const requestedRoot=path.resolve(runtimeRoot);
|
|
275
|
+
const requestedInfo=await lstat(requestedRoot,{bigint:true});
|
|
276
|
+
if(requestedInfo.isSymbolicLink()||!requestedInfo.isDirectory()){
|
|
277
|
+
fail('SDK runtime root must be a real directory.');
|
|
278
|
+
}
|
|
279
|
+
const canonicalRoot=await realpath(requestedRoot);
|
|
280
|
+
if(receipt.canonicalLocation!==canonicalRoot){
|
|
281
|
+
fail('Runtime verification receipt belongs to a different runtime location.');
|
|
282
|
+
}
|
|
283
|
+
await assertRuntimeState({
|
|
284
|
+
canonicalRoot,
|
|
285
|
+
rootIdentity:receipt.rootIdentity,
|
|
286
|
+
manifestPath:receipt.manifestPath,
|
|
287
|
+
manifestIdentity:receipt.manifestIdentity,
|
|
288
|
+
identities:receipt.identities,
|
|
289
|
+
signal
|
|
290
|
+
});
|
|
291
|
+
return receipt;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function readVerifiedRuntimeFile(receipt,{
|
|
295
|
+
runtimeRoot=path.join(sdkRoot,'runtime'),
|
|
296
|
+
relativePath,
|
|
297
|
+
signal
|
|
298
|
+
}={}){
|
|
299
|
+
if(!receipt||!issuedReceipts.has(receipt)){
|
|
300
|
+
fail('Runtime verification receipt was not issued by this SDK process.');
|
|
301
|
+
}
|
|
302
|
+
throwIfAborted(signal);
|
|
303
|
+
const normalized=safeInventoryPath(relativePath);
|
|
304
|
+
const key=value=>process.platform==='win32'?value.toLowerCase():value;
|
|
305
|
+
const file=receipt.files.find(candidate=>key(candidate.path)===key(normalized));
|
|
306
|
+
if(!file)fail(`Path is not in the verified runtime inventory: ${normalized}.`);
|
|
307
|
+
if(file.bytes>MAX_VERIFIED_RUNTIME_FILE_BYTES){
|
|
308
|
+
fail(
|
|
309
|
+
`Verified runtime file exceeds the ${MAX_VERIFIED_RUNTIME_FILE_BYTES}-byte development serving limit: ${file.path}.`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
const identity=receipt.identities.find(candidate=>key(candidate.path)===key(file.path));
|
|
313
|
+
if(!identity)fail(`Verified runtime identity is missing for ${file.path}.`);
|
|
314
|
+
|
|
315
|
+
const requestedRoot=path.resolve(runtimeRoot);
|
|
316
|
+
const requestedInfo=await lstat(requestedRoot,{bigint:true});
|
|
317
|
+
if(requestedInfo.isSymbolicLink()||!requestedInfo.isDirectory()){
|
|
318
|
+
fail('SDK runtime root must be a real directory.');
|
|
319
|
+
}
|
|
320
|
+
const canonicalRoot=await realpath(requestedRoot);
|
|
321
|
+
if(receipt.canonicalLocation!==canonicalRoot){
|
|
322
|
+
fail('Runtime verification receipt belongs to a different runtime location.');
|
|
323
|
+
}
|
|
324
|
+
const rootInfo=await lstat(canonicalRoot,{bigint:true});
|
|
325
|
+
if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()
|
|
326
|
+
||!identityMatches(rootInfo,receipt.rootIdentity)){
|
|
327
|
+
fail('SDK runtime root changed after verification.');
|
|
328
|
+
}
|
|
329
|
+
const filePath=path.resolve(canonicalRoot,...file.path.split('/'));
|
|
330
|
+
const relative=path.relative(canonicalRoot,filePath);
|
|
331
|
+
if(relative.startsWith('..')||path.isAbsolute(relative)){
|
|
332
|
+
fail(`Runtime receipt path escapes its root: ${file.path}.`);
|
|
333
|
+
}
|
|
334
|
+
const before=await lstat(filePath,{bigint:true});
|
|
335
|
+
if(before.isSymbolicLink()||!before.isFile()||!identityMatches(before,identity)){
|
|
336
|
+
fail(`SDK runtime file changed after verification: ${file.path}.`);
|
|
337
|
+
}
|
|
338
|
+
let handle;
|
|
339
|
+
try{
|
|
340
|
+
handle=await open(filePath,READ_ONLY_NO_FOLLOW);
|
|
341
|
+
}catch(error){
|
|
342
|
+
if(error?.code==='ELOOP')fail(`SDK runtime file became a symbolic link: ${file.path}.`);
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
try{
|
|
346
|
+
const opened=await handle.stat({bigint:true});
|
|
347
|
+
if(!opened.isFile()||!identityMatches(opened,identity)){
|
|
348
|
+
fail(`SDK runtime file changed while it was being opened: ${file.path}.`);
|
|
349
|
+
}
|
|
350
|
+
const bytes=await handle.readFile();
|
|
351
|
+
throwIfAborted(signal);
|
|
352
|
+
const after=await handle.stat({bigint:true});
|
|
353
|
+
if(!identityMatches(after,identity)||bytes.length!==file.bytes){
|
|
354
|
+
fail(`SDK runtime file changed while it was being read: ${file.path}.`);
|
|
355
|
+
}
|
|
356
|
+
if(createHash('sha256').update(bytes).digest('hex')!==file.sha256){
|
|
357
|
+
fail(`SDK runtime file hash changed: ${file.path}.`);
|
|
358
|
+
}
|
|
359
|
+
const current=await lstat(filePath,{bigint:true});
|
|
360
|
+
if(current.isSymbolicLink()||!current.isFile()||!identityMatches(current,identity)){
|
|
361
|
+
fail(`SDK runtime path changed while it was being read: ${file.path}.`);
|
|
362
|
+
}
|
|
363
|
+
const canonicalFile=await realpath(filePath);
|
|
364
|
+
const canonicalRelative=path.relative(canonicalRoot,canonicalFile);
|
|
365
|
+
if(canonicalRelative.startsWith('..')||path.isAbsolute(canonicalRelative)){
|
|
366
|
+
fail(`SDK runtime path left its root: ${file.path}.`);
|
|
367
|
+
}
|
|
368
|
+
return bytes;
|
|
369
|
+
}finally{
|
|
370
|
+
await handle.close();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export async function verifyRuntime({
|
|
375
|
+
runtimeRoot=path.join(sdkRoot,'runtime'),
|
|
376
|
+
signal,
|
|
377
|
+
onEvent
|
|
378
|
+
}={}){
|
|
379
|
+
throwIfAborted(signal);
|
|
380
|
+
const requestedRoot=path.resolve(runtimeRoot);
|
|
381
|
+
let requestedInfo;
|
|
382
|
+
try{requestedInfo=await lstat(requestedRoot,{bigint:true});}
|
|
383
|
+
catch(error){
|
|
384
|
+
if(error?.code==='ENOENT')fail(`SDK runtime root does not exist: ${requestedRoot}.`);
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
if(requestedInfo.isSymbolicLink()||!requestedInfo.isDirectory())fail('SDK runtime root must be a real directory.');
|
|
388
|
+
const canonicalRoot=await realpath(requestedRoot);
|
|
389
|
+
const rootInfo=await lstat(canonicalRoot,{bigint:true});
|
|
390
|
+
if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory())fail('SDK runtime root must be a real directory.');
|
|
391
|
+
const rootIdentity=fileIdentity(rootInfo);
|
|
392
|
+
const {release,manifestPath,manifestSha256,manifestIdentity}=await readRelease(canonicalRoot);
|
|
393
|
+
await emit(onEvent,{type:'runtime.verify.started',fileCount:release.fileCount,totalBytes:release.totalBytes});
|
|
394
|
+
const actualPaths=await listRuntimeFiles(canonicalRoot,{signal});
|
|
395
|
+
const expectedPaths=release.files.map(file=>file.path);
|
|
396
|
+
if(JSON.stringify(actualPaths)!==JSON.stringify(expectedPaths)){
|
|
397
|
+
fail('SDK runtime file inventory does not match ARCANE_RUNTIME_RELEASE.json.');
|
|
398
|
+
}
|
|
399
|
+
const identities=[];
|
|
400
|
+
let verifiedBytes=0;
|
|
401
|
+
for(const [index,file] of release.files.entries()){
|
|
402
|
+
throwIfAborted(signal);
|
|
403
|
+
const absolute=path.resolve(canonicalRoot,...file.path.split('/'));
|
|
404
|
+
const relative=path.relative(canonicalRoot,absolute);
|
|
405
|
+
if(relative.startsWith('..')||path.isAbsolute(relative))fail(`Runtime file escapes its root: ${file.path}.`);
|
|
406
|
+
const result=await hashExactFile(absolute,file.bytes,signal);
|
|
407
|
+
if(result.sha256!==file.sha256)fail(`SDK runtime integrity check failed for ${file.path}.`);
|
|
408
|
+
verifiedBytes+=file.bytes;
|
|
409
|
+
identities.push({path:file.path,...result.identity});
|
|
410
|
+
await emit(onEvent,{
|
|
411
|
+
type:'runtime.verify.progress',
|
|
412
|
+
current:index+1,
|
|
413
|
+
total:release.fileCount,
|
|
414
|
+
verifiedBytes,
|
|
415
|
+
totalBytes:release.totalBytes,
|
|
416
|
+
path:file.path
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const frozenIdentities=Object.freeze(identities.map(Object.freeze));
|
|
420
|
+
await assertRuntimeState({
|
|
421
|
+
canonicalRoot,
|
|
422
|
+
rootIdentity,
|
|
423
|
+
manifestPath,
|
|
424
|
+
manifestIdentity,
|
|
425
|
+
identities:frozenIdentities,
|
|
426
|
+
signal
|
|
427
|
+
});
|
|
428
|
+
const receipt=Object.freeze({
|
|
429
|
+
schemaVersion:1,
|
|
430
|
+
kind:'arcane-sdk-runtime-verification',
|
|
431
|
+
canonicalLocation:canonicalRoot,
|
|
432
|
+
rootIdentity,
|
|
433
|
+
manifestPath,
|
|
434
|
+
manifestSha256,
|
|
435
|
+
manifestIdentity,
|
|
436
|
+
sdkVersion:release.sdkVersion,
|
|
437
|
+
source:Object.freeze({...release.source}),
|
|
438
|
+
files:immutableFileInventory(release.files),
|
|
439
|
+
fileCount:release.fileCount,
|
|
440
|
+
totalBytes:release.totalBytes,
|
|
441
|
+
contentSha256:release.contentSha256,
|
|
442
|
+
identities:frozenIdentities
|
|
443
|
+
});
|
|
444
|
+
issuedReceipts.add(receipt);
|
|
445
|
+
await emit(onEvent,{
|
|
446
|
+
type:'runtime.verify.completed',
|
|
447
|
+
contentSha256:receipt.contentSha256,
|
|
448
|
+
fileCount:receipt.fileCount,
|
|
449
|
+
totalBytes:receipt.totalBytes
|
|
450
|
+
});
|
|
451
|
+
return receipt;
|
|
452
|
+
}
|