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.
Files changed (248) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/COMMERCIAL-LICENSE.md +13 -0
  3. package/LICENSE +661 -0
  4. package/NOTICE +70 -0
  5. package/README.md +448 -0
  6. package/bin/arcane-test.mjs +741 -0
  7. package/bin/arcane.mjs +5 -0
  8. package/docs/architecture.md +234 -0
  9. package/docs/compatibility.md +36 -0
  10. package/docs/event-manager.md +166 -0
  11. package/docs/platform-targets.md +108 -0
  12. package/docs/publishing.md +203 -0
  13. package/docs/reference/README.md +117 -0
  14. package/docs/reference/arcane-ollama.md +288 -0
  15. package/docs/reference/availability-and-normalization.md +123 -0
  16. package/docs/reference/behavioral-testing.md +86 -0
  17. package/docs/reference/cli.md +569 -0
  18. package/docs/reference/core/README.md +62 -0
  19. package/docs/reference/core/arcane-ai-contracts.md +873 -0
  20. package/docs/reference/core/arcane-api.md +601 -0
  21. package/docs/reference/core/arcane-entities.md +65 -0
  22. package/docs/reference/core/arcane-events.md +134 -0
  23. package/docs/reference/core/ollama-module.md +181 -0
  24. package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
  25. package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
  26. package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
  27. package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
  28. package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
  29. package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
  30. package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
  31. package/docs/reference/event-manager.md +957 -0
  32. package/docs/reference/inventory/package-api.json +2632 -0
  33. package/docs/reference/inventory/runtime-components.json +934 -0
  34. package/docs/reference/inventory/runtime-entities.json +26 -0
  35. package/docs/reference/inventory/runtime-modules.json +1249 -0
  36. package/docs/reference/protocols.md +242 -0
  37. package/docs/reference/runtime-components.md +1098 -0
  38. package/docs/reference/runtime-entities.md +303 -0
  39. package/docs/reference/runtime-modules.md +2010 -0
  40. package/docs/reference/sdk-api.md +4901 -0
  41. package/docs/roadmap.md +79 -0
  42. package/docs/work-amplification.md +124 -0
  43. package/node_modules/event-pubsub/CHANGELOG.md +55 -0
  44. package/node_modules/event-pubsub/MIGRATION.md +70 -0
  45. package/node_modules/event-pubsub/README.md +363 -0
  46. package/node_modules/event-pubsub/SECURITY.md +37 -0
  47. package/node_modules/event-pubsub/index.js +141 -0
  48. package/node_modules/event-pubsub/licence +21 -0
  49. package/node_modules/event-pubsub/package.json +59 -0
  50. package/node_modules/strong-type/README.md +408 -0
  51. package/node_modules/strong-type/assets/strong-type-header.png +0 -0
  52. package/node_modules/strong-type/index.js +1151 -0
  53. package/node_modules/strong-type/licence +21 -0
  54. package/node_modules/strong-type/node.js +125 -0
  55. package/node_modules/strong-type/package.json +61 -0
  56. package/package.json +95 -0
  57. package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
  58. package/runtime/arcane/components/app-bar.html +468 -0
  59. package/runtime/arcane/components/assistant-panel.html +715 -0
  60. package/runtime/arcane/components/calculator.html +8 -0
  61. package/runtime/arcane/components/chart.html +655 -0
  62. package/runtime/arcane/components/chat.html +1225 -0
  63. package/runtime/arcane/components/conversation-view.html +13 -0
  64. package/runtime/arcane/components/dashboard-config.html +341 -0
  65. package/runtime/arcane/components/data-maintenance.html +112 -0
  66. package/runtime/arcane/components/data-view.html +92 -0
  67. package/runtime/arcane/components/directory-picker.html +197 -0
  68. package/runtime/arcane/components/document-inspector.html +252 -0
  69. package/runtime/arcane/components/file-drop.html +264 -0
  70. package/runtime/arcane/components/file-inspector.html +293 -0
  71. package/runtime/arcane/components/file-manager.html +1715 -0
  72. package/runtime/arcane/components/header.html +142 -0
  73. package/runtime/arcane/components/integration-settings.html +14 -0
  74. package/runtime/arcane/components/local-ai-status.html +360 -0
  75. package/runtime/arcane/components/markdown-document.html +1048 -0
  76. package/runtime/arcane/components/markdown-editor.html +360 -0
  77. package/runtime/arcane/components/media-embed.html +8 -0
  78. package/runtime/arcane/components/modal.html +402 -0
  79. package/runtime/arcane/components/output-panel.html +259 -0
  80. package/runtime/arcane/components/preferences-form.html +135 -0
  81. package/runtime/arcane/components/record-timeline.html +105 -0
  82. package/runtime/arcane/components/relationship-board.html +116 -0
  83. package/runtime/arcane/components/screen-capture.html +8 -0
  84. package/runtime/arcane/components/source-code-viewer.html +441 -0
  85. package/runtime/arcane/components/source-explanation.html +124 -0
  86. package/runtime/arcane/components/speech.html +365 -0
  87. package/runtime/arcane/components/summary-strip.html +177 -0
  88. package/runtime/arcane/components/table.html +77 -0
  89. package/runtime/arcane/components/task-progress.html +282 -0
  90. package/runtime/arcane/components/terminal-workspace.html +65 -0
  91. package/runtime/arcane/components/theme-editor.html +41 -0
  92. package/runtime/arcane/components/theme-switcher.html +46 -0
  93. package/runtime/arcane/components/unified-inbox.html +20 -0
  94. package/runtime/arcane/components/voice-transcription.html +476 -0
  95. package/runtime/arcane/components/weather-widget.html +8 -0
  96. package/runtime/arcane/components/web-navigator.html +239 -0
  97. package/runtime/arcane/css/communications.css +1 -0
  98. package/runtime/arcane/css/dashboard-config.css +45 -0
  99. package/runtime/arcane/css/document-site.css +981 -0
  100. package/runtime/arcane/css/layout.css +438 -0
  101. package/runtime/arcane/css/primitives.css +321 -0
  102. package/runtime/arcane/css/theme.css +112 -0
  103. package/runtime/arcane/css/utility-workspace.css +1 -0
  104. package/runtime/arcane/entities/ApiModelRecord.js +20 -0
  105. package/runtime/arcane/entities/Calculation.js +13 -0
  106. package/runtime/arcane/entities/Chat.js +581 -0
  107. package/runtime/arcane/entities/CommunicationMessage.js +29 -0
  108. package/runtime/arcane/entities/CommunicationThread.js +21 -0
  109. package/runtime/arcane/entities/Document.js +10 -0
  110. package/runtime/arcane/entities/File.js +143 -0
  111. package/runtime/arcane/entities/Image.js +135 -0
  112. package/runtime/arcane/entities/IntentEnvelope.js +834 -0
  113. package/runtime/arcane/entities/Preference.js +83 -0
  114. package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
  115. package/runtime/arcane/entities/TerminalSession.js +46 -0
  116. package/runtime/arcane/entities/Theme.js +107 -0
  117. package/runtime/arcane/entities/User.js +1046 -0
  118. package/runtime/arcane/entities/Weather.js +23 -0
  119. package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
  120. package/runtime/arcane/img/arrow-left.png +0 -0
  121. package/runtime/arcane/img/arrow-right.png +0 -0
  122. package/runtime/arcane/img/doc.svg +5 -0
  123. package/runtime/arcane/img/folder.svg +4 -0
  124. package/runtime/arcane/img/image.svg +5 -0
  125. package/runtime/arcane/img/refresh.png +0 -0
  126. package/runtime/arcane/img/send.svg +9 -0
  127. package/runtime/arcane/img/trash.svg +5 -0
  128. package/runtime/arcane/img/upload.svg +5 -0
  129. package/runtime/arcane/modules/AI.js +2048 -0
  130. package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
  131. package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
  132. package/runtime/arcane/modules/AIResponseLength.js +42 -0
  133. package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
  134. package/runtime/arcane/modules/AnsiText.js +53 -0
  135. package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
  136. package/runtime/arcane/modules/AppDataScope.js +245 -0
  137. package/runtime/arcane/modules/AppearancePreferences.js +28 -0
  138. package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
  139. package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
  140. package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
  141. package/runtime/arcane/modules/AsyncBoundary.js +161 -0
  142. package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
  143. package/runtime/arcane/modules/CalculatorEngine.js +20 -0
  144. package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
  145. package/runtime/arcane/modules/ChartLibrary.js +35 -0
  146. package/runtime/arcane/modules/ChatRecords.js +13 -0
  147. package/runtime/arcane/modules/CommunicationAppController.js +43 -0
  148. package/runtime/arcane/modules/CommunicationHub.js +16 -0
  149. package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
  150. package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
  151. package/runtime/arcane/modules/ComponentContracts.js +586 -0
  152. package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
  153. package/runtime/arcane/modules/ConversationActionItems.js +488 -0
  154. package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
  155. package/runtime/arcane/modules/ConversationTimebox.js +527 -0
  156. package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
  157. package/runtime/arcane/modules/DBLS.js +171 -0
  158. package/runtime/arcane/modules/DBOPFS.js +1154 -0
  159. package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
  160. package/runtime/arcane/modules/DataMaintenance.js +91 -0
  161. package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
  162. package/runtime/arcane/modules/DirectoryPicker.js +109 -0
  163. package/runtime/arcane/modules/DocumentNavigation.js +223 -0
  164. package/runtime/arcane/modules/Errors.js +1025 -0
  165. package/runtime/arcane/modules/GifEncoder.js +29 -0
  166. package/runtime/arcane/modules/HTMLImport.js +114 -0
  167. package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
  168. package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
  169. package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
  170. package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
  171. package/runtime/arcane/modules/MD.js +111 -0
  172. package/runtime/arcane/modules/Mail.js +352 -0
  173. package/runtime/arcane/modules/MailTransport.mjs +180 -0
  174. package/runtime/arcane/modules/Marked.min.js +71 -0
  175. package/runtime/arcane/modules/MemoryRecords.js +44 -0
  176. package/runtime/arcane/modules/MessageAdvisory.js +38 -0
  177. package/runtime/arcane/modules/ModelDefinition.js +189 -0
  178. package/runtime/arcane/modules/Ollama.js +74 -0
  179. package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
  180. package/runtime/arcane/modules/OllamaSettings.js +24 -0
  181. package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
  182. package/runtime/arcane/modules/PreferenceStore.js +109 -0
  183. package/runtime/arcane/modules/QRCode.min.js +1 -0
  184. package/runtime/arcane/modules/Questionnaire.js +61 -0
  185. package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
  186. package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
  187. package/runtime/arcane/modules/RecordReviewStore.js +94 -0
  188. package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
  189. package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
  190. package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
  191. package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
  192. package/runtime/arcane/modules/ScreenCapture.js +20 -0
  193. package/runtime/arcane/modules/SpeechPlayback.js +581 -0
  194. package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
  195. package/runtime/arcane/modules/SystemAppearance.js +20 -0
  196. package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
  197. package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
  198. package/runtime/arcane/modules/TerminalClient.js +52 -0
  199. package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
  200. package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
  201. package/runtime/arcane/modules/ThemeManager.js +131 -0
  202. package/runtime/arcane/modules/TimeGuard.js +149 -0
  203. package/runtime/arcane/modules/ToolCallRouter.js +83 -0
  204. package/runtime/arcane/modules/WaitForComponent.js +102 -0
  205. package/runtime/arcane/modules/YouTubeMedia.js +16 -0
  206. package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
  207. package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
  208. package/runtime/arcane/modules/uPlot.min.css +1 -0
  209. package/runtime/arcane/security/arcane-network-policy.json +6 -0
  210. package/runtime/strong-type/index.js +352 -0
  211. package/runtime/strong-type/licence +21 -0
  212. package/runtime/strong-type/package.json +45 -0
  213. package/schemas/arcane-app-bundle.schema.json +125 -0
  214. package/schemas/arcane-app.schema.json +329 -0
  215. package/schemas/arcane-lock.schema.json +86 -0
  216. package/schemas/arcane-package.schema.json +224 -0
  217. package/schemas/cli-event.schema.json +122 -0
  218. package/schemas/event-stack.schema.json +152 -0
  219. package/schemas/native-build-plan.schema.json +176 -0
  220. package/schemas/target-adapter.schema.json +117 -0
  221. package/src/app-descriptor.mjs +500 -0
  222. package/src/cli/main.mjs +561 -0
  223. package/src/constants.mjs +31 -0
  224. package/src/dev-server.mjs +718 -0
  225. package/src/doctor.mjs +315 -0
  226. package/src/dom-event-instrumentation.mjs +594 -0
  227. package/src/errors.mjs +75 -0
  228. package/src/event-manager.mjs +1342 -0
  229. package/src/event-queue.mjs +138 -0
  230. package/src/events.mjs +219 -0
  231. package/src/index.mjs +177 -0
  232. package/src/integrated-provider-loader.mjs +432 -0
  233. package/src/native-plan.mjs +698 -0
  234. package/src/native-provider-loader.mjs +1126 -0
  235. package/src/packager/core.mjs +2691 -0
  236. package/src/process.mjs +353 -0
  237. package/src/release-bundle.mjs +2523 -0
  238. package/src/repository.mjs +90 -0
  239. package/src/runtime.mjs +452 -0
  240. package/src/scaffold.mjs +380 -0
  241. package/src/targets/index.mjs +436 -0
  242. package/src/templates/assets/app-icon.png +0 -0
  243. package/src/templates/workspace-template.mjs +388 -0
  244. package/src/testing-loader.mjs +9 -0
  245. package/src/testing.mjs +427 -0
  246. package/src/toolchain.mjs +1335 -0
  247. package/src/update-check.mjs +307 -0
  248. package/src/workspace.mjs +449 -0
@@ -0,0 +1,2691 @@
1
+ import {createHash,randomBytes} from 'node:crypto';
2
+ import {constants as FS_CONSTANTS} from 'node:fs';
3
+ import {
4
+ lstat,
5
+ mkdir,
6
+ open,
7
+ readFile,
8
+ readdir,
9
+ realpath,
10
+ rename,
11
+ rm,
12
+ stat,
13
+ writeFile
14
+ } from 'node:fs/promises';
15
+ import path from 'node:path';
16
+ import {pathToFileURL} from 'node:url';
17
+ import {setTimeout as delay} from 'node:timers/promises';
18
+ import {isDeepStrictEqual} from 'node:util';
19
+
20
+ export const ROOT_CONFIG_NAME='arcane-packager.json';
21
+ export const APP_CONFIG_NAME='arcane-package.json';
22
+ export const RELEASE_MANIFEST_NAME='ARCANE_APP_RELEASE.json';
23
+ export const PACKAGER_VERSION='arcane-app-packager-v1';
24
+
25
+ const RENAME_RETRY_CODES=new Set(['EACCES','EBUSY','EPERM']);
26
+ const RENAME_RETRY_LIMIT=20;
27
+ const RENAME_RETRY_DELAY_MS=250;
28
+ const READ_ONLY_NO_FOLLOW=FS_CONSTANTS.O_RDONLY|(FS_CONSTANTS.O_NOFOLLOW??0);
29
+ const MAX_VERIFIED_APP_FILE_BYTES=64*1024*1024;
30
+ const MAX_SHARED_SNAPSHOT_FILE_COUNT=10000;
31
+ const MAX_SHARED_SNAPSHOT_FILE_BYTES=64*1024*1024;
32
+ const MAX_SHARED_SNAPSHOT_TOTAL_BYTES=64*1024*1024;
33
+ const APP_DESCRIPTOR_NAME='arcane-app.json';
34
+ const LEGACY_APP_REGISTRY_PATH=path.join(
35
+ 'machine_bundles',
36
+ 'arcane-os-machine-bundle',
37
+ 'arcane-apps.json'
38
+ );
39
+ const issuedAppReleaseReceipts=new WeakMap();
40
+ const issuedSharedPayloadSnapshots=new WeakMap();
41
+ let appDescriptorContractsPromise;
42
+
43
+ function fileIdentity(info){
44
+ return Object.freeze({
45
+ device:String(info.dev),
46
+ inode:String(info.ino),
47
+ bytes:Number(info.size),
48
+ modifiedNanoseconds:String(info.mtimeNs),
49
+ changedNanoseconds:String(info.ctimeNs),
50
+ links:String(info.nlink)
51
+ });
52
+ }
53
+
54
+ function identityMatches(info,identity){
55
+ return String(info.dev)===identity.device
56
+ &&String(info.ino)===identity.inode
57
+ &&Number(info.size)===identity.bytes
58
+ &&String(info.mtimeNs)===identity.modifiedNanoseconds
59
+ &&String(info.ctimeNs)===identity.changedNanoseconds
60
+ &&String(info.nlink)===identity.links;
61
+ }
62
+
63
+ async function openStableRegularFile(filePath,label,expectedIdentity){
64
+ const before=await lstat(filePath,{bigint:true});
65
+ if(before.isSymbolicLink()||!before.isFile()){
66
+ fail(`${label} must be a regular file, not a link or special entry.`);
67
+ }
68
+ if(expectedIdentity&&!identityMatches(before,expectedIdentity)){
69
+ fail(`${label} changed after its package inventory was selected.`);
70
+ }
71
+
72
+ let handle;
73
+ try{
74
+ handle=await open(filePath,READ_ONLY_NO_FOLLOW);
75
+ }catch(error){
76
+ if(error?.code==='ELOOP')fail(`${label} became a symbolic link.`);
77
+ throw error;
78
+ }
79
+ try{
80
+ const opened=await handle.stat({bigint:true});
81
+ if(!opened.isFile()||!identityMatches(opened,fileIdentity(before))){
82
+ fail(`${label} changed while it was being opened.`);
83
+ }
84
+ return {handle,identity:fileIdentity(opened)};
85
+ }catch(error){
86
+ await handle.close().catch(()=>{});
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ async function readStableBytes(filePath,label,expectedIdentity){
92
+ const opened=await openStableRegularFile(filePath,label,expectedIdentity);
93
+ try{
94
+ const bytes=await opened.handle.readFile();
95
+ const after=await opened.handle.stat({bigint:true});
96
+ if(!identityMatches(after,opened.identity)){
97
+ fail(`${label} changed while it was being read.`);
98
+ }
99
+ return {bytes,identity:opened.identity};
100
+ }finally{
101
+ await opened.handle.close();
102
+ }
103
+ }
104
+
105
+ async function renamePackageDirectory(source,destination){
106
+ for(let attempt=0;;attempt++){
107
+ try{
108
+ await rename(source,destination);
109
+ return;
110
+ }catch(error){
111
+ if(!RENAME_RETRY_CODES.has(error?.code)||attempt>=RENAME_RETRY_LIMIT){
112
+ throw error;
113
+ }
114
+
115
+ await delay(RENAME_RETRY_DELAY_MS);
116
+ }
117
+ }
118
+ }
119
+
120
+ const APP_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
121
+ const SAFE_SHARED_ID_PATTERN=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
122
+ const WINDOWS_RESERVED_NAME=
123
+ /^(?:con|prn|aux|nul|clock\$|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\..*)?$/iu;
124
+ const FORBIDDEN_SEGMENTS=new Set([
125
+ '.agents',
126
+ '.codex',
127
+ '.git',
128
+ 'dist',
129
+ 'local'
130
+ ]);
131
+ const TEXT_CONTROL_PATTERN=/[\x00-\x1f\x7f]/;
132
+ const WINDOWS_UNSAFE_FILENAME_CHARACTER_PATTERN=/[<>"|?*]/u;
133
+ const OLLAMA_MODEL_IDENTIFIER=
134
+ /^[A-Za-z0-9][A-Za-z0-9._/-]{0,191}(?::[A-Za-z0-9][A-Za-z0-9._-]{0,63})?$/;
135
+ const MODEL_DEFINITION_PATTERN=/^(?:Modelfile|[A-Za-z0-9][A-Za-z0-9._-]{0,118}\.Modelfile)$/;
136
+ const MAX_LOCAL_AI_MODELS=64;
137
+ const MAX_MODEL_DEFINITION_BYTES=512*1024;
138
+
139
+ function fail(message,code){
140
+ const error=new Error(message);
141
+ if(code)error.code=code;
142
+ throw error;
143
+ }
144
+
145
+ function throwIfAborted(signal){
146
+ if(!signal?.aborted){
147
+ return;
148
+ }
149
+
150
+ const error=signal.reason instanceof Error
151
+ ?signal.reason
152
+ :new Error('Arcane package operation cancelled.');
153
+ if(!error.code){
154
+ error.code='ARCANE_CANCELLED';
155
+ }
156
+ throw error;
157
+ }
158
+
159
+ async function emitOperation(onEvent,event){
160
+ if(typeof onEvent==='function'){
161
+ await onEvent(event);
162
+ }
163
+ }
164
+
165
+ function isPlainObject(value){
166
+ return value!==null
167
+ &&typeof value==='object'
168
+ &&Object.getPrototypeOf(value)===Object.prototype;
169
+ }
170
+
171
+ function immutableJsonCopy(value){
172
+ if(Array.isArray(value)){
173
+ return Object.freeze(value.map(item=>immutableJsonCopy(item)));
174
+ }
175
+ if(isPlainObject(value)){
176
+ return Object.freeze(Object.fromEntries(
177
+ Object.entries(value).map(([key,item])=>[key,immutableJsonCopy(item)])
178
+ ));
179
+ }
180
+ return value;
181
+ }
182
+
183
+ function compareText(left,right){
184
+ return Buffer.compare(Buffer.from(String(left),'utf8'),Buffer.from(String(right),'utf8'));
185
+ }
186
+
187
+ function assertOnlyKeys(value,allowed,label){
188
+ if(!isPlainObject(value)){
189
+ fail(`${label} must be a JSON object.`);
190
+ }
191
+
192
+ for(const key of Object.keys(value)){
193
+ if(!allowed.has(key)){
194
+ fail(`${label} has an unsupported key: ${key}`);
195
+ }
196
+ }
197
+ }
198
+
199
+ function normalizeWorkspaceRoot(workspaceRoot){
200
+ if(typeof workspaceRoot!=='string'||!workspaceRoot.trim()){
201
+ fail('workspaceRoot must be a directory path.');
202
+ }
203
+
204
+ return path.resolve(workspaceRoot);
205
+ }
206
+
207
+ export function normalizeRelativePath(value,label='path'){
208
+ if(typeof value!=='string'||!value||value.includes('\\')||TEXT_CONTROL_PATTERN.test(value)){
209
+ fail(`Unsafe ${label}: ${String(value)}`);
210
+ }
211
+
212
+ if(path.posix.isAbsolute(value)||/^[a-z]:/i.test(value)){
213
+ fail(`Unsafe ${label}: ${value}`);
214
+ }
215
+
216
+ const segments=value.split('/');
217
+
218
+ for(const segment of segments){
219
+ if(!segment||segment==='.'||segment==='..'||segment.includes(':')
220
+ ||WINDOWS_UNSAFE_FILENAME_CHARACTER_PATTERN.test(segment)
221
+ ||segment.endsWith('.')||segment.endsWith(' ')
222
+ ||WINDOWS_RESERVED_NAME.test(segment)){
223
+ fail(`Unsafe ${label}: ${value}`);
224
+ }
225
+ }
226
+
227
+ return segments.join('/');
228
+ }
229
+
230
+ function normalizeRelativeRoot(value,label){
231
+ if(value==='.'){
232
+ return '.';
233
+ }
234
+
235
+ return normalizeRelativePath(value,label);
236
+ }
237
+
238
+ function isInside(root,candidate,{allowEqual=false}={}){
239
+ const relative=path.relative(path.resolve(root),path.resolve(candidate));
240
+ return (allowEqual&&relative==='')
241
+ ||Boolean(relative&&!relative.startsWith('..')&&!path.isAbsolute(relative));
242
+ }
243
+
244
+ function resolveInside(root,relative,label,{allowRoot=false}={}){
245
+ const normalized=relative==='.'&&allowRoot
246
+ ?'.'
247
+ :normalizeRelativePath(relative,label);
248
+ const candidate=path.resolve(root,...(normalized==='.'?[]:normalized.split('/')));
249
+
250
+ if(!isInside(root,candidate,{allowEqual:allowRoot})){
251
+ fail(`${label} leaves its allowed root: ${relative}`);
252
+ }
253
+
254
+ return candidate;
255
+ }
256
+
257
+ function pathKey(relative){
258
+ return relative.toLocaleLowerCase('en-US');
259
+ }
260
+
261
+ function pathIsSameOrDescendant(candidate,parent){
262
+ const candidateKey=pathKey(candidate);
263
+ const parentKey=pathKey(parent);
264
+ return candidateKey===parentKey||candidateKey.startsWith(`${parentKey}/`);
265
+ }
266
+
267
+ function isGlobLike(value){
268
+ return /[*?\[\]{}]/.test(value);
269
+ }
270
+
271
+ function validatePathList(value,label,{required=false}={}){
272
+ if(!Array.isArray(value)||(required&&value.length===0)){
273
+ fail(`${label} must be ${required?'a non-empty':'an'} array of literal relative paths.`);
274
+ }
275
+
276
+ if(value.length>512){
277
+ fail(`${label} is unreasonably large.`);
278
+ }
279
+
280
+ const normalized=value.map((entry,index)=>{
281
+ const item=normalizeRelativePath(entry,`${label}[${index}]`);
282
+
283
+ if(isGlobLike(item)){
284
+ fail(`${label}[${index}] must be literal; directories already include descendants.`);
285
+ }
286
+
287
+ return item;
288
+ });
289
+ const keys=new Set();
290
+
291
+ for(const item of normalized){
292
+ const key=pathKey(item);
293
+
294
+ if(keys.has(key)){
295
+ fail(`${label} contains a duplicate path: ${item}`);
296
+ }
297
+
298
+ keys.add(key);
299
+ }
300
+
301
+ if(required){
302
+ for(let left=0;left<normalized.length;left++){
303
+ for(let right=left+1;right<normalized.length;right++){
304
+ if(pathIsSameOrDescendant(normalized[left],normalized[right])
305
+ ||pathIsSameOrDescendant(normalized[right],normalized[left])){
306
+ fail(`${label} has overlapping paths: ${normalized[left]} and ${normalized[right]}`);
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ return normalized;
313
+ }
314
+
315
+ function isAlwaysForbidden(relative){
316
+ return relative.split('/').some(segment=>{
317
+ const key=pathKey(segment);
318
+ return FORBIDDEN_SEGMENTS.has(key)||key==='.env'||key.startsWith('.env.');
319
+ });
320
+ }
321
+
322
+ function isAppSourceForbidden(relative){
323
+ return isAlwaysForbidden(relative)
324
+ ||relative.split('/').some(segment=>pathKey(segment)==='node_modules');
325
+ }
326
+
327
+ function isExcluded(relative,excludes){
328
+ return isAlwaysForbidden(relative)
329
+ ||excludes.some(excluded=>pathIsSameOrDescendant(relative,excluded));
330
+ }
331
+
332
+ function assertSafePresentationText(value,label,maximum=160){
333
+ if(typeof value!=='string'||!value.trim()||value.length>maximum
334
+ ||TEXT_CONTROL_PATTERN.test(value)||/[<>]/.test(value)){
335
+ fail(`${label} must be plain text no longer than ${maximum} characters.`);
336
+ }
337
+
338
+ return value.trim();
339
+ }
340
+
341
+ export function parseSemver(value){
342
+ if(typeof value!=='string'){
343
+ fail(`Invalid semantic version: ${String(value)}`);
344
+ }
345
+
346
+ const match=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(value);
347
+
348
+ if(!match){
349
+ fail(`Invalid semantic version: ${value}`);
350
+ }
351
+
352
+ const prerelease=match[4]?match[4].split('.'):[];
353
+ const build=match[5]?match[5].split('.'):[];
354
+
355
+ for(const identifier of prerelease){
356
+ if(/^\d+$/.test(identifier)&&identifier.length>1&&identifier.startsWith('0')){
357
+ fail(`Invalid semantic version: ${value}`);
358
+ }
359
+ }
360
+
361
+ const numbers=match.slice(1,4).map(Number);
362
+
363
+ if(numbers.some(number=>!Number.isSafeInteger(number))){
364
+ fail(`Semantic version component exceeds JavaScript's safe integer range: ${value}`);
365
+ }
366
+
367
+ return {
368
+ major:numbers[0],
369
+ minor:numbers[1],
370
+ patch:numbers[2],
371
+ prerelease,
372
+ build
373
+ };
374
+ }
375
+
376
+ function formatSemver(version){
377
+ let rendered=`${version.major}.${version.minor}.${version.patch}`;
378
+
379
+ if(version.prerelease?.length){
380
+ rendered+=`-${version.prerelease.join('.')}`;
381
+ }
382
+
383
+ if(version.build?.length){
384
+ rendered+=`+${version.build.join('.')}`;
385
+ }
386
+
387
+ return rendered;
388
+ }
389
+
390
+ function validatePreid(preid){
391
+ const value=preid??'rc';
392
+
393
+ if(typeof value!=='string'||!/^[0-9A-Za-z-]+$/.test(value)
394
+ ||(/^\d+$/.test(value)&&value.length>1&&value.startsWith('0'))){
395
+ fail(`Invalid prerelease identifier: ${String(value)}`);
396
+ }
397
+
398
+ return value;
399
+ }
400
+
401
+ export function incrementSemver(value,bump,preid){
402
+ const current=parseSemver(value);
403
+
404
+ if(!['major','minor','patch','prerelease'].includes(bump)){
405
+ fail(`Unsupported semantic version bump: ${String(bump)}`);
406
+ }
407
+
408
+ if(bump==='major'){
409
+ return formatSemver({major:current.major+1,minor:0,patch:0});
410
+ }
411
+
412
+ if(bump==='minor'){
413
+ return formatSemver({major:current.major,minor:current.minor+1,patch:0});
414
+ }
415
+
416
+ if(bump==='patch'){
417
+ return formatSemver({major:current.major,minor:current.minor,patch:current.patch+1});
418
+ }
419
+
420
+ const requestedPreid=validatePreid(preid);
421
+ const next={
422
+ major:current.major,
423
+ minor:current.minor,
424
+ patch:current.patch,
425
+ prerelease:[]
426
+ };
427
+
428
+ if(!current.prerelease.length){
429
+ next.patch+=1;
430
+ next.prerelease=[requestedPreid,'0'];
431
+ return formatSemver(next);
432
+ }
433
+
434
+ if(current.prerelease[0]!==requestedPreid){
435
+ next.prerelease=[requestedPreid,'0'];
436
+ return formatSemver(next);
437
+ }
438
+
439
+ next.prerelease=[...current.prerelease];
440
+ let incremented=false;
441
+
442
+ for(let index=next.prerelease.length-1;index>=0;index--){
443
+ if(/^\d+$/.test(next.prerelease[index])){
444
+ const number=Number(next.prerelease[index]);
445
+
446
+ if(!Number.isSafeInteger(number)||number===Number.MAX_SAFE_INTEGER){
447
+ fail(`Prerelease number is too large to increment: ${value}`);
448
+ }
449
+
450
+ next.prerelease[index]=String(number+1);
451
+ incremented=true;
452
+ break;
453
+ }
454
+ }
455
+
456
+ if(!incremented){
457
+ next.prerelease.push('0');
458
+ }
459
+
460
+ return formatSemver(next);
461
+ }
462
+
463
+ async function readJsonDocument(filePath,label=filePath,{expectedIdentity}={}){
464
+ let document;
465
+ try{
466
+ document=await readStableBytes(filePath,label,expectedIdentity);
467
+ }catch(error){
468
+ if(error?.code==='ENOENT')fail(`${label} does not exist.`);
469
+ throw error;
470
+ }
471
+
472
+ try{
473
+ return {
474
+ value:JSON.parse(document.bytes.toString('utf8')),
475
+ bytes:document.bytes,
476
+ identity:document.identity
477
+ };
478
+ }catch(error){
479
+ fail(`${label} is not valid JSON: ${error.message}`);
480
+ }
481
+ }
482
+
483
+ async function readJson(filePath,label=filePath){
484
+ return (await readJsonDocument(filePath,label)).value;
485
+ }
486
+
487
+ function validateSharedRoute(route,label){
488
+ assertOnlyKeys(route,new Set(['source','destination','include','exclude']),label);
489
+ const source=normalizeRelativeRoot(route.source,`${label}.source`);
490
+ const destination=normalizeRelativeRoot(route.destination,`${label}.destination`);
491
+ const include=validatePathList(route.include,`${label}.include`,{required:true});
492
+ const exclude=validatePathList(route.exclude??[],`${label}.exclude`);
493
+
494
+ if(source==='.'||source==='apps'||source.startsWith('apps/')
495
+ ||source==='dist'||source.startsWith('dist/')
496
+ ||source==='node_modules'
497
+ ||isAlwaysForbidden(source)){
498
+ fail(`${label}.source is outside the permitted shared-payload boundary: ${source}`);
499
+ }
500
+
501
+ if(destination==='apps'||destination.startsWith('apps/')
502
+ ||destination===RELEASE_MANIFEST_NAME){
503
+ fail(`${label}.destination overlaps a reserved package path: ${destination}`);
504
+ }
505
+
506
+ return Object.freeze({source,destination,include,exclude});
507
+ }
508
+
509
+ async function loadRootConfigDocument(workspaceRoot){
510
+ const configPath=path.join(workspaceRoot,ROOT_CONFIG_NAME);
511
+ const document=await readJsonDocument(configPath,ROOT_CONFIG_NAME);
512
+ return {
513
+ ...document,
514
+ value:validateRootConfig(document.value,configPath),
515
+ configPath
516
+ };
517
+ }
518
+
519
+ async function loadRootConfig(workspaceRoot){
520
+ return (await loadRootConfigDocument(workspaceRoot)).value;
521
+ }
522
+
523
+ export function validateRootConfig(value,configPath=ROOT_CONFIG_NAME){
524
+ assertOnlyKeys(value,new Set(['schemaVersion','appsRoot','distRoot','sharedPayloads']),ROOT_CONFIG_NAME);
525
+
526
+ if(value.schemaVersion!==1){
527
+ fail(`${ROOT_CONFIG_NAME}.schemaVersion must be 1.`);
528
+ }
529
+
530
+ if(value.appsRoot!=='apps'||value.distRoot!=='dist'){
531
+ fail(`${ROOT_CONFIG_NAME} must bind appsRoot to "apps" and distRoot to "dist".`);
532
+ }
533
+
534
+ if(!isPlainObject(value.sharedPayloads)){
535
+ fail(`${ROOT_CONFIG_NAME}.sharedPayloads must be an object.`);
536
+ }
537
+
538
+ const sharedPayloads={};
539
+
540
+ for(const [id,routes] of Object.entries(value.sharedPayloads).sort(([left],[right])=>compareText(left,right))){
541
+ if(!SAFE_SHARED_ID_PATTERN.test(id)){
542
+ fail(`Unsafe shared payload id: ${id}`);
543
+ }
544
+
545
+ if(!Array.isArray(routes)||routes.length===0){
546
+ fail(`sharedPayloads.${id} must be a non-empty array.`);
547
+ }
548
+
549
+ sharedPayloads[id]=Object.freeze(routes.map((route,index)=>
550
+ validateSharedRoute(route,`sharedPayloads.${id}[${index}]`)
551
+ ));
552
+ }
553
+
554
+ return Object.freeze({
555
+ schemaVersion:1,
556
+ appsRoot:'apps',
557
+ distRoot:'dist',
558
+ sharedPayloads:Object.freeze(sharedPayloads),
559
+ configPath
560
+ });
561
+ }
562
+
563
+ export function validateAppConfig(value,appId,rootConfig,configPath=`apps/${appId}/${APP_CONFIG_NAME}`){
564
+ assertOnlyKeys(
565
+ value,
566
+ new Set([
567
+ 'schemaVersion',
568
+ 'id',
569
+ 'displayName',
570
+ 'version',
571
+ 'entry',
572
+ 'strategy',
573
+ 'security',
574
+ 'localAIModelPolicy',
575
+ 'include',
576
+ 'exclude',
577
+ 'shared',
578
+ 'adapter'
579
+ ]),
580
+ `${appId}/${APP_CONFIG_NAME}`
581
+ );
582
+
583
+ if(value.schemaVersion!==1){
584
+ fail(`${appId}/${APP_CONFIG_NAME}.schemaVersion must be 1.`);
585
+ }
586
+
587
+ if(value.id!==appId||!APP_ID_PATTERN.test(value.id)){
588
+ fail(`${appId}/${APP_CONFIG_NAME}.id must exactly match its apps directory.`);
589
+ }
590
+
591
+ const displayName=assertSafePresentationText(
592
+ value.displayName,
593
+ `${appId}/${APP_CONFIG_NAME}.displayName`
594
+ );
595
+ parseSemver(value.version);
596
+ const entry=normalizeRelativePath(value.entry,`${appId}/${APP_CONFIG_NAME}.entry`);
597
+ const include=validatePathList(value.include,`${appId}/${APP_CONFIG_NAME}.include`,{required:true});
598
+ const exclude=validatePathList(value.exclude??[],`${appId}/${APP_CONFIG_NAME}.exclude`);
599
+
600
+ if(include.some(allowed=>pathIsSameOrDescendant(APP_CONFIG_NAME,allowed))){
601
+ fail(`${appId}/${APP_CONFIG_NAME}.include must not expose the authored package configuration.`);
602
+ }
603
+
604
+ const localAIModelPolicy=value.localAIModelPolicy===undefined
605
+ ?Object.freeze({verified_only:true,models:Object.freeze([])})
606
+ :validateLocalAIModelPolicy(value.localAIModelPolicy,`${appId}/${APP_CONFIG_NAME}.localAIModelPolicy`);
607
+ const security=validateAppSecurity(value.security,appId);
608
+
609
+ for(const model of localAIModelPolicy.models){
610
+ if(isAlwaysForbidden(model.definition)||isExcluded(model.definition,exclude)
611
+ ||!include.some(allowed=>pathIsSameOrDescendant(model.definition,allowed))){
612
+ fail(`${appId}/${APP_CONFIG_NAME}.localAIModelPolicy model definition is not covered by its public include rules: ${model.definition}`);
613
+ }
614
+ }
615
+
616
+ if(isAlwaysForbidden(entry)||isExcluded(entry,exclude)
617
+ ||!include.some(allowed=>pathIsSameOrDescendant(entry,allowed))){
618
+ fail(`${appId}/${APP_CONFIG_NAME}.entry is not covered by its public include rules.`);
619
+ }
620
+
621
+ if(!['static','adapter'].includes(value.strategy)){
622
+ fail(`${appId}/${APP_CONFIG_NAME}.strategy must be "static" or "adapter".`);
623
+ }
624
+
625
+ if(!Array.isArray(value.shared)||new Set(value.shared).size!==value.shared.length){
626
+ fail(`${appId}/${APP_CONFIG_NAME}.shared must be an array of unique shared payload ids.`);
627
+ }
628
+
629
+ for(const [index,sharedId] of value.shared.entries()){
630
+ if(typeof sharedId!=='string'||!Object.hasOwn(rootConfig.sharedPayloads,sharedId)){
631
+ fail(`${appId}/${APP_CONFIG_NAME}.shared[${index}] references an unknown shared payload: ${String(sharedId)}`);
632
+ }
633
+ }
634
+
635
+ let adapter=null;
636
+
637
+ if(value.strategy==='adapter'){
638
+ adapter=normalizeRelativePath(value.adapter,`${appId}/${APP_CONFIG_NAME}.adapter`);
639
+
640
+ if(!adapter.startsWith('scripts/')||path.posix.extname(adapter)!=='.mjs'){
641
+ fail(`${appId}/${APP_CONFIG_NAME}.adapter must be an app-local scripts/*.mjs module.`);
642
+ }
643
+ }else if(value.adapter!==undefined){
644
+ fail(`${appId}/${APP_CONFIG_NAME}.adapter is only valid with strategy "adapter".`);
645
+ }
646
+
647
+ return Object.freeze({
648
+ schemaVersion:1,
649
+ id:appId,
650
+ displayName,
651
+ version:value.version,
652
+ entry,
653
+ strategy:value.strategy,
654
+ security,
655
+ localAIModelPolicy,
656
+ include:Object.freeze(include),
657
+ exclude:Object.freeze(exclude),
658
+ shared:Object.freeze([...value.shared]),
659
+ adapter,
660
+ configPath
661
+ });
662
+ }
663
+
664
+ function validateOriginList(value,label,{allowLoopbackHttp=false,allowHttpsScheme=false}={}){
665
+ if(!Array.isArray(value)||value.length>16){
666
+ fail(`${label} must be an array with at most 16 origins.`);
667
+ }
668
+ const origins=value.map((origin,index)=>{
669
+ if(typeof origin!=='string'||origin!==origin.trim()){
670
+ fail(`${label}[${index}] is invalid.`);
671
+ }
672
+ if(origin==='https:'&&allowHttpsScheme)return origin;
673
+ let parsed;
674
+ try{
675
+ parsed=new URL(origin);
676
+ }catch{
677
+ fail(`${label}[${index}] is not a valid URL origin.`);
678
+ }
679
+ if(parsed.origin!==origin||parsed.hostname.endsWith('.')||parsed.username||parsed.password
680
+ ||parsed.pathname!=='/'||parsed.search||parsed.hash){
681
+ fail(`${label}[${index}] must be a canonical allowed origin.`);
682
+ }
683
+ if(parsed.protocol==='http:'){
684
+ if(!allowLoopbackHttp||!['127.0.0.1','[::1]'].includes(parsed.hostname)){
685
+ fail(`${label}[${index}] may use HTTP only for a numeric loopback host.`);
686
+ }
687
+ }else if(parsed.protocol!=='https:'){
688
+ fail(`${label}[${index}] must use HTTPS or an approved loopback HTTP origin.`);
689
+ }
690
+ return parsed.origin;
691
+ });
692
+ if(new Set(origins).size!==origins.length)fail(`${label} must not contain duplicates.`);
693
+ if(JSON.stringify(origins)!==JSON.stringify([...origins].sort(compareText))){
694
+ fail(`${label} must be sorted for deterministic projection.`);
695
+ }
696
+ return Object.freeze(origins);
697
+ }
698
+
699
+ function validateAppSecurity(value,appId){
700
+ if(!isPlainObject(value))fail(`${appId}/${APP_CONFIG_NAME}.security must be an object.`);
701
+ const label=`${appId}/${APP_CONFIG_NAME}.security`;
702
+ assertOnlyKeys(value,new Set(['connectOrigins','frameOrigins','mediaOrigins']),label);
703
+ return Object.freeze({
704
+ connectOrigins:validateOriginList(value.connectOrigins,`${label}.connectOrigins`,{allowLoopbackHttp:true}),
705
+ frameOrigins:validateOriginList(value.frameOrigins,`${label}.frameOrigins`,{allowHttpsScheme:appId==='browser'}),
706
+ mediaOrigins:validateOriginList(value.mediaOrigins,`${label}.mediaOrigins`)
707
+ });
708
+ }
709
+
710
+ function validateLocalAIModelPolicy(value,label){
711
+ assertOnlyKeys(value,new Set(['verified_only','models']),label);
712
+
713
+ if(typeof value.verified_only!=='boolean'){
714
+ fail(`${label}.verified_only must be a boolean.`);
715
+ }
716
+
717
+ if(!Array.isArray(value.models)){
718
+ fail(`${label}.models must be an array.`);
719
+ }
720
+
721
+ if(value.models.length>MAX_LOCAL_AI_MODELS){
722
+ fail(`${label}.models must contain no more than ${MAX_LOCAL_AI_MODELS} entries.`);
723
+ }
724
+
725
+ const modelNames=new Set();
726
+ const definitions=new Set();
727
+ const models=value.models.map((model,index)=>{
728
+ const modelLabel=`${label}.models[${index}]`;
729
+ assertOnlyKeys(model,new Set(['name','definition']),modelLabel);
730
+
731
+ if(typeof model.name!=='string'||model.name!==model.name.trim()
732
+ ||!OLLAMA_MODEL_IDENTIFIER.test(model.name)||model.name.toUpperCase()==='OPENAI'){
733
+ fail(`${modelLabel}.name must be a canonical bounded Ollama model identifier.`);
734
+ }
735
+
736
+ if(typeof model.definition!=='string'||model.definition.length>128
737
+ ||!MODEL_DEFINITION_PATTERN.test(model.definition)
738
+ ||normalizeRelativePath(model.definition,`${modelLabel}.definition`)!==model.definition){
739
+ fail(`${modelLabel}.definition must be a safe app-relative Modelfile basename.`);
740
+ }
741
+
742
+ const canonicalName=model.name.toLocaleLowerCase('en-US');
743
+ const canonicalAlias=canonicalName.includes(':')?canonicalName:`${canonicalName}:latest`;
744
+ const definitionKey=pathKey(model.definition);
745
+
746
+ if(modelNames.has(canonicalAlias)){
747
+ fail(`${label}.models contains a duplicate canonical model name: ${model.name}`);
748
+ }
749
+ if(definitions.has(definitionKey)){
750
+ fail(`${label}.models contains a duplicate definition: ${model.definition}`);
751
+ }
752
+
753
+ modelNames.add(canonicalAlias);
754
+ definitions.add(definitionKey);
755
+ return Object.freeze({name:model.name,definition:model.definition});
756
+ });
757
+
758
+ return Object.freeze({
759
+ verified_only:value.verified_only,
760
+ models:Object.freeze(models)
761
+ });
762
+ }
763
+
764
+ async function validateLocalAIModelDefinitions(appRoot,config){
765
+ for(const [index,model] of config.localAIModelPolicy.models.entries()){
766
+ const label=`${config.id}/${APP_CONFIG_NAME}.localAIModelPolicy.models[${index}].definition`;
767
+ const definitionPath=resolveInside(appRoot,model.definition,label);
768
+ let details;
769
+
770
+ try{
771
+ await assertNoLinks(appRoot,definitionPath,label);
772
+ details=await lstat(definitionPath);
773
+ }catch(error){
774
+ if(error?.code==='ENOENT'){
775
+ fail(`${label} does not exist: ${model.definition}`);
776
+ }
777
+ throw error;
778
+ }
779
+
780
+ if(details.isSymbolicLink()||!details.isFile()||details.size<1
781
+ ||details.size>MAX_MODEL_DEFINITION_BYTES){
782
+ fail(`${label} must be a non-empty regular file no larger than 512 KiB.`);
783
+ }
784
+ }
785
+ }
786
+
787
+ async function assertNoLinks(root,candidate,label){
788
+ const resolvedRoot=path.resolve(root);
789
+ const resolvedCandidate=path.resolve(candidate);
790
+
791
+ if(!isInside(resolvedRoot,resolvedCandidate,{allowEqual:true})){
792
+ fail(`${label} leaves its allowed root.`);
793
+ }
794
+
795
+ const relative=path.relative(resolvedRoot,resolvedCandidate);
796
+ let current=resolvedRoot;
797
+ const rootInfo=await lstat(resolvedRoot);
798
+
799
+ if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()){
800
+ fail(`${label} root must be a real directory.`);
801
+ }
802
+
803
+ for(const segment of relative.split(path.sep).filter(Boolean)){
804
+ current=path.join(current,segment);
805
+ const info=await lstat(current);
806
+
807
+ if(info.isSymbolicLink()){
808
+ fail(`${label} contains a symbolic link or junction: ${current}`);
809
+ }
810
+ }
811
+
812
+ const [actualRoot,actualCandidate]=await Promise.all([
813
+ realpath(resolvedRoot),
814
+ realpath(resolvedCandidate)
815
+ ]);
816
+
817
+ if(!isInside(actualRoot,actualCandidate,{allowEqual:true})){
818
+ fail(`${label} resolves outside its allowed root.`);
819
+ }
820
+ }
821
+
822
+ async function assertSafeDistBoundary(workspaceRoot,distRoot,{create=false}={}){
823
+ await assertNoLinks(workspaceRoot,workspaceRoot,'workspace');
824
+ let details;
825
+
826
+ try{
827
+ details=await lstat(distRoot);
828
+ }catch(error){
829
+ if(error?.code!=='ENOENT'){
830
+ throw error;
831
+ }
832
+
833
+ if(!create){
834
+ return false;
835
+ }
836
+
837
+ try{
838
+ await mkdir(distRoot);
839
+ }catch(createError){
840
+ if(createError?.code!=='EEXIST'){
841
+ throw createError;
842
+ }
843
+ }
844
+
845
+ details=await lstat(distRoot);
846
+ }
847
+
848
+ if(details.isSymbolicLink()||!details.isDirectory()){
849
+ fail('dist must be a real workspace directory, not a link, junction, or special entry.');
850
+ }
851
+
852
+ await assertNoLinks(workspaceRoot,distRoot,'dist');
853
+ return true;
854
+ }
855
+
856
+ async function assertOptionalSafeOutput(distRoot,outputRoot,appId){
857
+ let details;
858
+
859
+ try{
860
+ details=await lstat(outputRoot);
861
+ }catch(error){
862
+ if(error?.code==='ENOENT'){
863
+ return false;
864
+ }
865
+
866
+ throw error;
867
+ }
868
+
869
+ if(details.isSymbolicLink()||!details.isDirectory()){
870
+ fail(`dist/${appId} must be a real directory, not a link, junction, or special entry.`);
871
+ }
872
+
873
+ await assertNoLinks(distRoot,outputRoot,`dist/${appId}`);
874
+ return true;
875
+ }
876
+
877
+ async function appDescriptorContracts(){
878
+ appDescriptorContractsPromise??=import('../app-descriptor.mjs');
879
+ return appDescriptorContractsPromise;
880
+ }
881
+
882
+ async function optionalRegularFileIdentity(filePath,label){
883
+ let info;
884
+ try{
885
+ info=await lstat(filePath,{bigint:true});
886
+ }catch(error){
887
+ if(error?.code==='ENOENT')return null;
888
+ throw error;
889
+ }
890
+ if(info.isSymbolicLink()||!info.isFile()){
891
+ fail(`${label} must be a regular file, not a link or special entry.`);
892
+ }
893
+ return fileIdentity(info);
894
+ }
895
+
896
+ function recordedIdentityMatches(left,right){
897
+ if(left===null||right===null)return left===right;
898
+ return left.device===right.device
899
+ &&left.inode===right.inode
900
+ &&left.bytes===right.bytes
901
+ &&left.modifiedNanoseconds===right.modifiedNanoseconds
902
+ &&left.changedNanoseconds===right.changedNanoseconds
903
+ &&left.links===right.links;
904
+ }
905
+
906
+ async function createAppDescriptorAuthority(context,packageDocument,{signal}={}){
907
+ throwIfAborted(signal);
908
+ const contracts=await appDescriptorContracts();
909
+ const descriptorPath=path.join(context.appRoot,APP_DESCRIPTOR_NAME);
910
+ const descriptorIdentity=await optionalRegularFileIdentity(
911
+ descriptorPath,
912
+ `apps/${context.config.id}/${APP_DESCRIPTOR_NAME}`
913
+ );
914
+ let descriptor;
915
+ let source;
916
+ let sourcePath;
917
+ let sourceIdentity;
918
+
919
+ if(descriptorIdentity){
920
+ const descriptorDocument=await readJsonDocument(
921
+ descriptorPath,
922
+ `apps/${context.config.id}/${APP_DESCRIPTOR_NAME}`,
923
+ {expectedIdentity:descriptorIdentity}
924
+ );
925
+ descriptor=contracts.validateAppDescriptor(descriptorDocument.value,{
926
+ appId:context.config.id
927
+ });
928
+ if(!isDeepStrictEqual(
929
+ contracts.projectPackageManifest(descriptor),
930
+ packageDocument.value
931
+ )){
932
+ fail(`${APP_DESCRIPTOR_NAME} does not project exactly to ${APP_CONFIG_NAME}.`);
933
+ }
934
+ source='authored';
935
+ sourcePath=descriptorPath;
936
+ sourceIdentity=descriptorDocument.identity;
937
+ }else{
938
+ const registryPath=path.join(context.workspaceRoot,LEGACY_APP_REGISTRY_PATH);
939
+ const registryBefore=await optionalRegularFileIdentity(
940
+ registryPath,
941
+ 'Arcane native app registry'
942
+ );
943
+ const loaded=await contracts.loadAppDescriptor({
944
+ workspaceRoot:context.workspaceRoot,
945
+ appRoot:context.appRoot,
946
+ appId:context.config.id,
947
+ packageManifest:packageDocument.value
948
+ });
949
+ const [descriptorAfter,registryAfter]=await Promise.all([
950
+ optionalRegularFileIdentity(
951
+ descriptorPath,
952
+ `apps/${context.config.id}/${APP_DESCRIPTOR_NAME}`
953
+ ),
954
+ optionalRegularFileIdentity(registryPath,'Arcane native app registry')
955
+ ]);
956
+ if(descriptorAfter!==null||!recordedIdentityMatches(registryBefore,registryAfter)){
957
+ fail(`The descriptor source for ${context.config.id} changed while it was selected.`);
958
+ }
959
+ descriptor=loaded.descriptor;
960
+ source=loaded.source;
961
+ sourcePath=registryBefore?registryPath:null;
962
+ sourceIdentity=registryBefore;
963
+ }
964
+
965
+ const canonicalDescriptor=immutableJsonCopy(descriptor);
966
+ return Object.freeze({
967
+ descriptor:canonicalDescriptor,
968
+ descriptorSha256:contracts.appDescriptorSha256(canonicalDescriptor),
969
+ source,
970
+ sourcePath,
971
+ sourceIdentity,
972
+ packageConfigPath:context.config.configPath,
973
+ packageConfigIdentity:packageDocument.identity
974
+ });
975
+ }
976
+
977
+ async function assertAppDescriptorAuthorityCurrent(context,{signal}={}){
978
+ const expected=context.descriptorAuthority;
979
+ if(!expected)fail('Canonical app descriptor authority is unavailable.');
980
+ const packageDocument=await readJsonDocument(
981
+ expected.packageConfigPath,
982
+ expected.packageConfigPath,
983
+ {expectedIdentity:expected.packageConfigIdentity}
984
+ );
985
+ const config=validateAppConfig(
986
+ packageDocument.value,
987
+ context.config.id,
988
+ context.rootConfig,
989
+ expected.packageConfigPath
990
+ );
991
+ const current=await createAppDescriptorAuthority(
992
+ {...context,config},
993
+ packageDocument,
994
+ {signal}
995
+ );
996
+ if(current.descriptorSha256!==expected.descriptorSha256
997
+ ||current.source!==expected.source
998
+ ||current.sourcePath!==expected.sourcePath
999
+ ||!recordedIdentityMatches(current.sourceIdentity,expected.sourceIdentity)
1000
+ ||!recordedIdentityMatches(
1001
+ current.packageConfigIdentity,
1002
+ expected.packageConfigIdentity
1003
+ )){
1004
+ fail(`The canonical descriptor authority for ${context.config.id} changed during packaging.`);
1005
+ }
1006
+ return expected;
1007
+ }
1008
+
1009
+ async function assertValidatedDescriptorAuthority(validation,descriptorAuthority){
1010
+ if(validation===undefined||validation?.app?.descriptor===undefined)return;
1011
+ const descriptor=validation?.app?.descriptor;
1012
+ const contracts=await appDescriptorContracts();
1013
+ if(contracts.appDescriptorSha256(descriptor)!==descriptorAuthority.descriptorSha256){
1014
+ fail('Source validation returned a different canonical Arcane application descriptor.');
1015
+ }
1016
+ }
1017
+
1018
+ async function getAppContext({
1019
+ workspaceRoot:requestedWorkspaceRoot,
1020
+ appId,
1021
+ bindDescriptorAuthority=false,
1022
+ signal
1023
+ }){
1024
+ const workspaceRoot=normalizeWorkspaceRoot(requestedWorkspaceRoot);
1025
+
1026
+ if(typeof appId!=='string'||!APP_ID_PATTERN.test(appId)){
1027
+ fail(`Invalid app id: ${String(appId)}`);
1028
+ }
1029
+
1030
+ const rootConfig=await loadRootConfig(workspaceRoot);
1031
+ const appsRoot=path.join(workspaceRoot,rootConfig.appsRoot);
1032
+ const appRoot=resolveInside(appsRoot,appId,'app id');
1033
+ let appInfo;
1034
+
1035
+ try{
1036
+ appInfo=await lstat(appRoot);
1037
+ }catch(error){
1038
+ if(error?.code==='ENOENT'){
1039
+ const available=(await readdir(appsRoot,{withFileTypes:true}))
1040
+ .filter(entry=>entry.isDirectory()&&APP_ID_PATTERN.test(entry.name))
1041
+ .map(entry=>entry.name)
1042
+ .sort(compareText);
1043
+ fail(`Unknown app "${appId}". Available apps: ${available.join(', ')||'[none]'}.`);
1044
+ }
1045
+
1046
+ throw error;
1047
+ }
1048
+
1049
+ if(appInfo.isSymbolicLink()||!appInfo.isDirectory()){
1050
+ fail(`apps/${appId} must be a real directory, not a link or special entry.`);
1051
+ }
1052
+
1053
+ await assertNoLinks(appsRoot,appRoot,`apps/${appId}`);
1054
+ const configPath=path.join(appRoot,APP_CONFIG_NAME);
1055
+ const configDocument=await readJsonDocument(
1056
+ configPath,
1057
+ `apps/${appId}/${APP_CONFIG_NAME}`
1058
+ );
1059
+ const config=validateAppConfig(configDocument.value,appId,rootConfig,configPath);
1060
+ await validateLocalAIModelDefinitions(appRoot,config);
1061
+ const distRoot=path.join(workspaceRoot,rootConfig.distRoot);
1062
+ const outputRoot=resolveInside(distRoot,appId,'package output');
1063
+ const distExists=await assertSafeDistBoundary(workspaceRoot,distRoot);
1064
+
1065
+ if(distExists){
1066
+ await assertOptionalSafeOutput(distRoot,outputRoot,appId);
1067
+ }
1068
+
1069
+ const context={workspaceRoot,rootConfig,appsRoot,appRoot,distRoot,outputRoot,config};
1070
+ if(!bindDescriptorAuthority)return context;
1071
+ return {
1072
+ ...context,
1073
+ descriptorAuthority:await createAppDescriptorAuthority(context,configDocument,{signal})
1074
+ };
1075
+ }
1076
+
1077
+ async function enumerateRoute({
1078
+ workspaceRoot,
1079
+ sourceRoot,
1080
+ destinationRoot,
1081
+ include,
1082
+ exclude,
1083
+ label,
1084
+ appPayload=false,
1085
+ signal
1086
+ }){
1087
+ throwIfAborted(signal);
1088
+ await assertNoLinks(workspaceRoot,sourceRoot,`${label}.source`);
1089
+ const files=[];
1090
+
1091
+ async function visit(absolute,relative){
1092
+ throwIfAborted(signal);
1093
+ if(isExcluded(relative,exclude)||(appPayload&&isAppSourceForbidden(relative))){
1094
+ return;
1095
+ }
1096
+
1097
+ const info=await lstat(absolute,{bigint:true});
1098
+
1099
+ if(info.isSymbolicLink()){
1100
+ fail(`${label} contains a symbolic link or junction: ${relative}`);
1101
+ }
1102
+
1103
+ if(info.isDirectory()){
1104
+ const entries=await readdir(absolute,{withFileTypes:true});
1105
+
1106
+ for(const entry of entries.sort((left,right)=>compareText(left.name,right.name))){
1107
+ throwIfAborted(signal);
1108
+ const childRelative=`${relative}/${entry.name}`;
1109
+
1110
+ if(isExcluded(childRelative,exclude)
1111
+ ||(appPayload&&isAppSourceForbidden(childRelative))){
1112
+ continue;
1113
+ }
1114
+
1115
+ if(entry.isSymbolicLink()){
1116
+ fail(`${label} contains a symbolic link or junction: ${childRelative}`);
1117
+ }
1118
+
1119
+ await visit(path.join(absolute,entry.name),childRelative);
1120
+ }
1121
+
1122
+ return;
1123
+ }
1124
+
1125
+ if(!info.isFile()){
1126
+ fail(`${label} contains a non-file entry: ${relative}`);
1127
+ }
1128
+
1129
+ const destination=destinationRoot==='.'
1130
+ ?relative
1131
+ :`${destinationRoot}/${relative}`;
1132
+ const bytes=Number(info.size);
1133
+ if(!Number.isSafeInteger(bytes)||bytes<0){
1134
+ fail(`${label} contains a file whose size is not safely representable: ${relative}`);
1135
+ }
1136
+ files.push({
1137
+ source:absolute,
1138
+ sourceRelative:relative,
1139
+ destination:normalizeRelativePath(destination,`${label} destination`),
1140
+ bytes,
1141
+ identity:fileIdentity(info),
1142
+ label
1143
+ });
1144
+ }
1145
+
1146
+ for(const allowed of include){
1147
+ throwIfAborted(signal);
1148
+ if(isExcluded(allowed,exclude)||(appPayload&&isAppSourceForbidden(allowed))){
1149
+ continue;
1150
+ }
1151
+
1152
+ const candidate=resolveInside(sourceRoot,allowed,`${label}.include`);
1153
+
1154
+ try{
1155
+ await assertNoLinks(sourceRoot,candidate,`${label}.include "${allowed}"`);
1156
+ }catch(error){
1157
+ if(error?.code==='ENOENT'){
1158
+ fail(`${label}.include does not exist: ${allowed}`);
1159
+ }
1160
+
1161
+ throw error;
1162
+ }
1163
+
1164
+ await visit(candidate,allowed);
1165
+ }
1166
+
1167
+ return files;
1168
+ }
1169
+
1170
+ function workspaceLocationIdentity(info){
1171
+ return Object.freeze({
1172
+ device:String(info.dev),
1173
+ inode:String(info.ino)
1174
+ });
1175
+ }
1176
+
1177
+ function workspaceLocationMatches(info,identity){
1178
+ return String(info.dev)===identity.device&&String(info.ino)===identity.inode;
1179
+ }
1180
+
1181
+ function normalizeSharedPayloadSelection(sharedPayloadIds,rootConfig,{required=false}={}){
1182
+ const selected=sharedPayloadIds===undefined
1183
+ ?Object.keys(rootConfig.sharedPayloads)
1184
+ :sharedPayloadIds;
1185
+
1186
+ if(!Array.isArray(selected)||selected.length>256||(required&&selected.length===0)){
1187
+ fail('sharedPayloadIds must be a non-empty array with at most 256 entries.');
1188
+ }
1189
+
1190
+ const normalized=[];
1191
+ const seen=new Set();
1192
+ for(const [index,id] of selected.entries()){
1193
+ if(typeof id!=='string'||!SAFE_SHARED_ID_PATTERN.test(id)
1194
+ ||!Object.hasOwn(rootConfig.sharedPayloads,id)){
1195
+ fail(`sharedPayloadIds[${index}] references an unknown shared payload: ${String(id)}`);
1196
+ }
1197
+ if(seen.has(id))fail(`sharedPayloadIds contains a duplicate shared payload: ${id}`);
1198
+ seen.add(id);
1199
+ normalized.push(id);
1200
+ }
1201
+ return Object.freeze(normalized.sort(compareText));
1202
+ }
1203
+
1204
+ function assertSnapshotCoverage(state,requiredIds){
1205
+ for(const id of requiredIds){
1206
+ if(!Object.hasOwn(state.filesBySharedPayload,id)){
1207
+ fail(`Shared payload snapshot does not include the required payload: ${id}`);
1208
+ }
1209
+ }
1210
+ }
1211
+
1212
+ async function authenticateSharedPayloadSnapshotState(receipt,{
1213
+ workspaceRoot,
1214
+ sharedPayloadIds,
1215
+ signal
1216
+ }={}){
1217
+ throwIfAborted(signal);
1218
+ const state=issuedSharedPayloadSnapshots.get(receipt);
1219
+ if(!state)fail('Shared payload snapshot was not issued by this SDK process.');
1220
+ const requested=normalizeWorkspaceRoot(workspaceRoot);
1221
+ let workspaceInfo;
1222
+ let canonicalWorkspaceRoot;
1223
+ try{
1224
+ workspaceInfo=await lstat(requested,{bigint:true});
1225
+ canonicalWorkspaceRoot=await realpath(requested);
1226
+ }catch(error){
1227
+ fail(`Shared payload snapshot workspace is unavailable: ${error.message}`);
1228
+ }
1229
+ if(workspaceInfo.isSymbolicLink()||!workspaceInfo.isDirectory()
1230
+ ||canonicalWorkspaceRoot!==state.canonicalWorkspaceRoot
1231
+ ||receipt.canonicalWorkspaceRoot!==canonicalWorkspaceRoot
1232
+ ||!workspaceLocationMatches(workspaceInfo,state.workspaceIdentity)){
1233
+ fail('Shared payload snapshot belongs to a different workspace identity.');
1234
+ }
1235
+ let configInfo;
1236
+ try{
1237
+ configInfo=await lstat(state.rootConfigPath,{bigint:true});
1238
+ }catch(error){
1239
+ fail(`Shared payload snapshot root configuration changed: ${error.message}`);
1240
+ }
1241
+ if(configInfo.isSymbolicLink()||!configInfo.isFile()
1242
+ ||!identityMatches(configInfo,state.rootConfigIdentity)){
1243
+ fail('Shared payload snapshot root configuration changed after preparation.');
1244
+ }
1245
+ if(sharedPayloadIds!==undefined){
1246
+ if(!Array.isArray(sharedPayloadIds)||sharedPayloadIds.length>256){
1247
+ fail('sharedPayloadIds must be an array with at most 256 entries.');
1248
+ }
1249
+ const seen=new Set();
1250
+ for(const [index,id] of sharedPayloadIds.entries()){
1251
+ if(typeof id!=='string'||!SAFE_SHARED_ID_PATTERN.test(id)||seen.has(id)){
1252
+ fail(`sharedPayloadIds[${index}] is invalid or duplicated.`);
1253
+ }
1254
+ seen.add(id);
1255
+ }
1256
+ assertSnapshotCoverage(state,sharedPayloadIds);
1257
+ }
1258
+ throwIfAborted(signal);
1259
+ return state;
1260
+ }
1261
+
1262
+ export async function prepareSharedPayloadSnapshot({
1263
+ workspaceRoot:requestedWorkspaceRoot,
1264
+ sharedPayloadIds,
1265
+ signal,
1266
+ onEvent
1267
+ }={}){
1268
+ await emitOperation(onEvent,{type:'shared-payload.snapshot.started'});
1269
+ throwIfAborted(signal);
1270
+ const resolvedWorkspaceRoot=normalizeWorkspaceRoot(requestedWorkspaceRoot);
1271
+ const initialWorkspaceInfo=await lstat(resolvedWorkspaceRoot,{bigint:true});
1272
+ if(initialWorkspaceInfo.isSymbolicLink()||!initialWorkspaceInfo.isDirectory()){
1273
+ fail('Shared payload snapshot workspace must be a real directory.');
1274
+ }
1275
+ const canonicalWorkspaceRoot=await realpath(resolvedWorkspaceRoot);
1276
+ await assertNoLinks(canonicalWorkspaceRoot,canonicalWorkspaceRoot,'shared payload snapshot workspace');
1277
+ const rootConfigDocument=await loadRootConfigDocument(canonicalWorkspaceRoot);
1278
+ const selectedIds=normalizeSharedPayloadSelection(
1279
+ sharedPayloadIds,
1280
+ rootConfigDocument.value,
1281
+ {required:true}
1282
+ );
1283
+ const retained=[];
1284
+ const retainedBySharedPayload=Object.fromEntries(selectedIds.map(id=>[id,[]]));
1285
+ let totalBytes=0;
1286
+ let completedFiles=0;
1287
+
1288
+ for(const sharedPayloadId of selectedIds){
1289
+ const routes=rootConfigDocument.value.sharedPayloads[sharedPayloadId];
1290
+ for(const [routeIndex,route] of routes.entries()){
1291
+ throwIfAborted(signal);
1292
+ const sourceRoot=resolveInside(
1293
+ canonicalWorkspaceRoot,
1294
+ route.source,
1295
+ `sharedPayloads.${sharedPayloadId}[${routeIndex}].source`
1296
+ );
1297
+ const files=await enumerateRoute({
1298
+ workspaceRoot:canonicalWorkspaceRoot,
1299
+ sourceRoot,
1300
+ destinationRoot:route.destination,
1301
+ include:route.include,
1302
+ exclude:route.exclude,
1303
+ label:`sharedPayloads.${sharedPayloadId}[${routeIndex}]`,
1304
+ signal
1305
+ });
1306
+ for(const file of files){
1307
+ throwIfAborted(signal);
1308
+ if(retained.length>=MAX_SHARED_SNAPSHOT_FILE_COUNT){
1309
+ fail(`Shared payload snapshot exceeds ${MAX_SHARED_SNAPSHOT_FILE_COUNT} files.`);
1310
+ }
1311
+ if(file.bytes>MAX_SHARED_SNAPSHOT_FILE_BYTES){
1312
+ fail(`Shared payload snapshot file exceeds ${MAX_SHARED_SNAPSHOT_FILE_BYTES} bytes: ${file.sourceRelative}`);
1313
+ }
1314
+ if(totalBytes+file.bytes>MAX_SHARED_SNAPSHOT_TOTAL_BYTES){
1315
+ fail(`Shared payload snapshot exceeds ${MAX_SHARED_SNAPSHOT_TOTAL_BYTES} retained bytes.`);
1316
+ }
1317
+ const stable=await readStableBytes(file.source,file.label,file.identity);
1318
+ if(stable.bytes.length!==file.bytes){
1319
+ fail(`Shared payload snapshot file changed size while retained: ${file.sourceRelative}`);
1320
+ }
1321
+ const digest=createHash('sha256').update(stable.bytes).digest('hex');
1322
+ const source=route.source==='.'
1323
+ ?file.sourceRelative
1324
+ :`${route.source}/${file.sourceRelative}`;
1325
+ const record=Object.freeze({
1326
+ ...file,
1327
+ sharedPayloadId,
1328
+ routeIndex,
1329
+ sourceRelative:normalizeRelativePath(source,'shared payload snapshot source'),
1330
+ retainedBytes:stable.bytes,
1331
+ sha256:digest
1332
+ });
1333
+ retained.push(record);
1334
+ retainedBySharedPayload[sharedPayloadId].push(record);
1335
+ totalBytes+=stable.bytes.length;
1336
+ completedFiles+=1;
1337
+ await emitOperation(onEvent,{
1338
+ type:'shared-payload.snapshot.progress',
1339
+ current:completedFiles,
1340
+ completedBytes:totalBytes,
1341
+ sharedPayloadId,
1342
+ path:record.sourceRelative
1343
+ });
1344
+ }
1345
+ }
1346
+ }
1347
+
1348
+ const [finalWorkspaceInfo,finalConfigInfo]=await Promise.all([
1349
+ lstat(canonicalWorkspaceRoot,{bigint:true}),
1350
+ lstat(rootConfigDocument.configPath,{bigint:true})
1351
+ ]);
1352
+ const workspaceIdentity=workspaceLocationIdentity(initialWorkspaceInfo);
1353
+ if(finalWorkspaceInfo.isSymbolicLink()||!finalWorkspaceInfo.isDirectory()
1354
+ ||!workspaceLocationMatches(finalWorkspaceInfo,workspaceIdentity)
1355
+ ||finalConfigInfo.isSymbolicLink()||!finalConfigInfo.isFile()
1356
+ ||!identityMatches(finalConfigInfo,rootConfigDocument.identity)){
1357
+ fail('Shared payload snapshot workspace or root configuration changed during preparation.');
1358
+ }
1359
+
1360
+ const inventory=retained.map(record=>({
1361
+ sharedPayloadId:record.sharedPayloadId,
1362
+ routeIndex:record.routeIndex,
1363
+ source:record.sourceRelative,
1364
+ destination:record.destination,
1365
+ bytes:record.bytes,
1366
+ sha256:record.sha256
1367
+ })).sort((left,right)=>compareText(JSON.stringify(left),JSON.stringify(right)));
1368
+ const receipt=immutableJsonCopy({
1369
+ schemaVersion:1,
1370
+ kind:'arcane-shared-payload-snapshot',
1371
+ canonicalWorkspaceRoot,
1372
+ workspaceIdentity,
1373
+ rootConfig:Object.freeze({
1374
+ path:ROOT_CONFIG_NAME,
1375
+ identity:rootConfigDocument.identity,
1376
+ sha256:createHash('sha256').update(rootConfigDocument.bytes).digest('hex')
1377
+ }),
1378
+ sharedPayloadIds:selectedIds,
1379
+ files:inventory,
1380
+ fileCount:inventory.length,
1381
+ totalBytes,
1382
+ contentSha256:createHash('sha256').update(JSON.stringify(inventory)).digest('hex')
1383
+ });
1384
+ const filesBySharedPayload=Object.freeze(Object.fromEntries(
1385
+ Object.entries(retainedBySharedPayload).map(([id,files])=>[id,Object.freeze(files)])
1386
+ ));
1387
+ issuedSharedPayloadSnapshots.set(receipt,Object.freeze({
1388
+ receipt,
1389
+ canonicalWorkspaceRoot,
1390
+ workspaceIdentity,
1391
+ rootConfigPath:rootConfigDocument.configPath,
1392
+ rootConfigIdentity:rootConfigDocument.identity,
1393
+ files:Object.freeze(retained),
1394
+ filesBySharedPayload
1395
+ }));
1396
+ await emitOperation(onEvent,{
1397
+ type:'shared-payload.snapshot.completed',
1398
+ fileCount:receipt.fileCount,
1399
+ totalBytes:receipt.totalBytes,
1400
+ contentSha256:receipt.contentSha256
1401
+ });
1402
+ return receipt;
1403
+ }
1404
+
1405
+ export async function authenticateSharedPayloadSnapshot(receipt,options={}){
1406
+ await authenticateSharedPayloadSnapshotState(receipt,options);
1407
+ return receipt;
1408
+ }
1409
+
1410
+ async function collectPackageFiles(context,{signal,sharedPayloadState}={}){
1411
+ const {workspaceRoot,appRoot,config,rootConfig}=context;
1412
+ const files=await enumerateRoute({
1413
+ workspaceRoot,
1414
+ sourceRoot:appRoot,
1415
+ destinationRoot:`apps/${config.id}`,
1416
+ include:config.include,
1417
+ exclude:config.exclude,
1418
+ label:`apps.${config.id}`,
1419
+ appPayload:true,
1420
+ signal
1421
+ });
1422
+
1423
+ if(sharedPayloadState){
1424
+ assertSnapshotCoverage(sharedPayloadState,config.shared);
1425
+ for(const sharedId of config.shared){
1426
+ files.push(...sharedPayloadState.filesBySharedPayload[sharedId]);
1427
+ }
1428
+ }else{
1429
+ for(const sharedId of config.shared){
1430
+ const routes=rootConfig.sharedPayloads[sharedId];
1431
+
1432
+ for(const [index,route] of routes.entries()){
1433
+ const sourceRoot=resolveInside(
1434
+ workspaceRoot,
1435
+ route.source,
1436
+ `sharedPayloads.${sharedId}[${index}].source`
1437
+ );
1438
+ files.push(...await enumerateRoute({
1439
+ workspaceRoot,
1440
+ sourceRoot,
1441
+ destinationRoot:route.destination,
1442
+ include:route.include,
1443
+ exclude:route.exclude,
1444
+ label:`sharedPayloads.${sharedId}[${index}]`,
1445
+ signal
1446
+ }));
1447
+ }
1448
+ }
1449
+ }
1450
+
1451
+ const destinations=new Map();
1452
+
1453
+ for(const file of files){
1454
+ throwIfAborted(signal);
1455
+ if(file.destination===RELEASE_MANIFEST_NAME||file.destination==='index.html'){
1456
+ fail(`${file.label} collides with generated package path: ${file.destination}`);
1457
+ }
1458
+
1459
+ const key=pathKey(file.destination);
1460
+
1461
+ if(destinations.has(key)){
1462
+ fail(`Package destination collision: ${file.destination} from ${file.source} and ${destinations.get(key).source}.`);
1463
+ }
1464
+
1465
+ destinations.set(key,file);
1466
+ }
1467
+
1468
+ const expectedEntry=`apps/${config.id}/${config.entry}`;
1469
+
1470
+ if(!destinations.has(pathKey(expectedEntry))){
1471
+ fail(`The configured entry file was not found in the package payload: ${expectedEntry}`);
1472
+ }
1473
+
1474
+ return files.sort((left,right)=>compareText(left.destination,right.destination));
1475
+ }
1476
+
1477
+ async function copyPackageFiles(files,outputRoot,{signal,onEvent}={}){
1478
+ let completedBytes=0;
1479
+ const buffer=Buffer.allocUnsafe(1024*1024);
1480
+
1481
+ for(const [index,file] of files.entries()){
1482
+ throwIfAborted(signal);
1483
+ const destination=resolveInside(outputRoot,file.destination,'package destination');
1484
+ await mkdir(path.dirname(destination),{recursive:true});
1485
+ if(file.retainedBytes!==undefined){
1486
+ if(!Buffer.isBuffer(file.retainedBytes)||file.retainedBytes.length!==file.bytes){
1487
+ fail(`Retained shared payload bytes are invalid for ${file.destination}.`);
1488
+ }
1489
+ const output=await open(destination,'wx');
1490
+ let copiedBytes=0;
1491
+ try{
1492
+ throwIfAborted(signal);
1493
+ await output.writeFile(file.retainedBytes);
1494
+ copiedBytes=file.retainedBytes.length;
1495
+ const outputAfter=await output.stat({bigint:true});
1496
+ if(Number(outputAfter.size)!==copiedBytes){
1497
+ fail(`Could not finish writing retained shared payload ${file.destination}.`);
1498
+ }
1499
+ }finally{
1500
+ await output.close().catch(()=>{});
1501
+ }
1502
+ completedBytes+=copiedBytes;
1503
+ await emitOperation(onEvent,{
1504
+ type:'package.copy.progress',
1505
+ current:index+1,
1506
+ total:files.length,
1507
+ completedBytes,
1508
+ path:file.destination
1509
+ });
1510
+ continue;
1511
+ }
1512
+ const source=await openStableRegularFile(file.source,file.label,file.identity);
1513
+ let output;
1514
+ let copiedBytes=0;
1515
+ try{
1516
+ output=await open(destination,'wx');
1517
+ while(true){
1518
+ throwIfAborted(signal);
1519
+ const {bytesRead}=await source.handle.read(buffer,0,buffer.length,null);
1520
+ if(bytesRead===0)break;
1521
+ let written=0;
1522
+ while(written<bytesRead){
1523
+ const result=await output.write(buffer,written,bytesRead-written,null);
1524
+ if(result.bytesWritten<=0)fail(`Could not finish writing ${file.destination}.`);
1525
+ written+=result.bytesWritten;
1526
+ }
1527
+ copiedBytes+=bytesRead;
1528
+ }
1529
+ const [sourceAfter,outputAfter]=await Promise.all([
1530
+ source.handle.stat({bigint:true}),
1531
+ output.stat({bigint:true})
1532
+ ]);
1533
+ if(!identityMatches(sourceAfter,file.identity)||copiedBytes!==file.bytes
1534
+ ||Number(outputAfter.size)!==copiedBytes){
1535
+ fail(`${file.label} changed while ${file.destination} was being copied.`);
1536
+ }
1537
+ }finally{
1538
+ await output?.close().catch(()=>{});
1539
+ await source.handle.close().catch(()=>{});
1540
+ }
1541
+ completedBytes+=copiedBytes;
1542
+ await emitOperation(onEvent,{
1543
+ type:'package.copy.progress',
1544
+ current:index+1,
1545
+ total:files.length,
1546
+ completedBytes,
1547
+ path:file.destination
1548
+ });
1549
+ }
1550
+ }
1551
+
1552
+ function escapeHtml(value){
1553
+ return String(value)
1554
+ .replaceAll('&','&amp;')
1555
+ .replaceAll('<','&lt;')
1556
+ .replaceAll('>','&gt;')
1557
+ .replaceAll('"','&quot;');
1558
+ }
1559
+
1560
+ async function materializeBasePackage(context,outputRoot,files,{signal,onEvent}={}){
1561
+ throwIfAborted(signal);
1562
+ await mkdir(outputRoot,{recursive:true});
1563
+ await assertNoLinks(context.distRoot,outputRoot,'package staging root');
1564
+ await copyPackageFiles(files,outputRoot,{signal,onEvent});
1565
+ throwIfAborted(signal);
1566
+ const start=`./apps/${context.config.id}/${context.config.entry}`;
1567
+ const title=escapeHtml(context.config.displayName);
1568
+ await writeFile(
1569
+ path.join(outputRoot,'index.html'),
1570
+ [
1571
+ '<!doctype html>',
1572
+ '<meta charset="utf-8">',
1573
+ `<meta http-equiv="refresh" content="0; url=${escapeHtml(start)}">`,
1574
+ `<title>${title}</title>`,
1575
+ `<a href="${escapeHtml(start)}">Open ${title}</a>`,
1576
+ ''
1577
+ ].join('\n'),
1578
+ 'utf8'
1579
+ );
1580
+ }
1581
+
1582
+ async function sha256WithIdentity(filePath,{signal,expectedIdentity,label=filePath}={}){
1583
+ throwIfAborted(signal);
1584
+ const hash=createHash('sha256');
1585
+ const opened=await openStableRegularFile(filePath,label,expectedIdentity);
1586
+ const buffer=Buffer.allocUnsafe(1024*1024);
1587
+
1588
+ try{
1589
+ while(true){
1590
+ throwIfAborted(signal);
1591
+ const {bytesRead}=await opened.handle.read(buffer,0,buffer.length,null);
1592
+
1593
+ if(bytesRead===0){
1594
+ break;
1595
+ }
1596
+
1597
+ hash.update(buffer.subarray(0,bytesRead));
1598
+ }
1599
+ const after=await opened.handle.stat({bigint:true});
1600
+ if(!identityMatches(after,opened.identity))fail(`${label} changed while it was being hashed.`);
1601
+ }finally{
1602
+ await opened.handle.close();
1603
+ }
1604
+
1605
+ return {sha256:hash.digest('hex'),identity:opened.identity};
1606
+ }
1607
+
1608
+ async function sha256(filePath,options={}){
1609
+ return (await sha256WithIdentity(filePath,options)).sha256;
1610
+ }
1611
+
1612
+ async function listOutputFiles(root,{signal}={}){
1613
+ const files=[];
1614
+
1615
+ async function visit(directory,relativeRoot=''){
1616
+ throwIfAborted(signal);
1617
+ const entries=await readdir(directory,{withFileTypes:true});
1618
+
1619
+ for(const entry of entries.sort((left,right)=>compareText(left.name,right.name))){
1620
+ throwIfAborted(signal);
1621
+ const relative=relativeRoot?`${relativeRoot}/${entry.name}`:entry.name;
1622
+ const absolute=path.join(directory,entry.name);
1623
+
1624
+ if(isAlwaysForbidden(relative)){
1625
+ fail(`Package contains a globally forbidden path: ${relative}`);
1626
+ }
1627
+ if(path.posix.basename(relative)===APP_CONFIG_NAME){
1628
+ fail(`Package contains authored configuration that must remain outside the browser payload: ${relative}`);
1629
+ }
1630
+
1631
+ if(entry.isSymbolicLink()){
1632
+ fail(`Package contains a symbolic link or junction: ${relative}`);
1633
+ }
1634
+
1635
+ if(entry.isDirectory()){
1636
+ await visit(absolute,relative);
1637
+ }else if(entry.isFile()){
1638
+ files.push({absolute,relative});
1639
+ }else{
1640
+ fail(`Package contains a non-file entry: ${relative}`);
1641
+ }
1642
+ }
1643
+ }
1644
+
1645
+ await visit(root);
1646
+ return files.sort((left,right)=>compareText(left.relative,right.relative));
1647
+ }
1648
+
1649
+ async function inventoryEntries(root,{signal,onEvent}={}){
1650
+ const files=(await listOutputFiles(root,{signal})).filter(file=>
1651
+ file.relative!==RELEASE_MANIFEST_NAME
1652
+ );
1653
+ const entries=[];
1654
+ const identities=[];
1655
+
1656
+ let completedBytes=0;
1657
+
1658
+ for(const [index,file] of files.entries()){
1659
+ throwIfAborted(signal);
1660
+ const verified=await sha256WithIdentity(file.absolute,{
1661
+ signal,
1662
+ label:`package file ${file.relative}`
1663
+ });
1664
+ entries.push({
1665
+ path:file.relative,
1666
+ bytes:verified.identity.bytes,
1667
+ sha256:verified.sha256
1668
+ });
1669
+ identities.push(Object.freeze({path:file.relative,...verified.identity}));
1670
+ completedBytes+=verified.identity.bytes;
1671
+ await emitOperation(onEvent,{
1672
+ type:'package.hash.progress',
1673
+ current:index+1,
1674
+ total:files.length,
1675
+ completedBytes,
1676
+ path:file.relative
1677
+ });
1678
+ }
1679
+
1680
+ return {entries,identities};
1681
+ }
1682
+
1683
+ async function assertArtifactState(root,identities,{signal}={}){
1684
+ throwIfAborted(signal);
1685
+ const requested=path.resolve(root);
1686
+ const rootBefore=await lstat(requested,{bigint:true});
1687
+ if(rootBefore.isSymbolicLink()||!rootBefore.isDirectory()){
1688
+ fail('App release root must be a real directory.');
1689
+ }
1690
+ const canonical=await realpath(requested);
1691
+ const actualPaths=(await listOutputFiles(canonical,{signal}))
1692
+ .map(file=>file.relative)
1693
+ .sort(compareText);
1694
+ const expectedPaths=identities.map(identity=>identity.path).sort(compareText);
1695
+ if(JSON.stringify(actualPaths)!==JSON.stringify(expectedPaths)){
1696
+ fail('App release inventory changed after verification.');
1697
+ }
1698
+
1699
+ for(const identity of identities){
1700
+ throwIfAborted(signal);
1701
+ const filePath=resolveInside(canonical,identity.path,'verified app release path');
1702
+ const info=await lstat(filePath,{bigint:true});
1703
+ if(info.isSymbolicLink()||!info.isFile()||!identityMatches(info,identity)){
1704
+ fail(`App release file changed after verification: ${identity.path}`);
1705
+ }
1706
+ }
1707
+ const rootAfter=await lstat(canonical,{bigint:true});
1708
+ if(!rootAfter.isDirectory()||rootAfter.isSymbolicLink()
1709
+ ||!identityMatches(rootAfter,fileIdentity(rootBefore))){
1710
+ fail('App release root changed while its verification state was authenticated.');
1711
+ }
1712
+ return {canonical,rootIdentity:fileIdentity(rootAfter)};
1713
+ }
1714
+
1715
+ function appReleasePackageBinding(config){
1716
+ if(!isPlainObject(config)){
1717
+ fail('App release package binding is missing.');
1718
+ }
1719
+ return immutableJsonCopy({
1720
+ schemaVersion:1,
1721
+ id:config.id,
1722
+ displayName:config.displayName,
1723
+ version:config.version,
1724
+ entry:config.entry,
1725
+ strategy:config.strategy,
1726
+ security:config.security,
1727
+ localAIModelPolicy:config.localAIModelPolicy??{verified_only:true,models:[]},
1728
+ include:[...(config.include??[])],
1729
+ exclude:[...(config.exclude??[])],
1730
+ shared:[...(config.shared??[])],
1731
+ adapter:config.adapter??null
1732
+ });
1733
+ }
1734
+
1735
+ async function issueAppReleaseReceipt(root,release,identities,{
1736
+ signal,
1737
+ packageConfig,
1738
+ descriptorAuthority
1739
+ }={}){
1740
+ const state=await assertArtifactState(root,identities,{signal});
1741
+ const packageBinding=appReleasePackageBinding(packageConfig);
1742
+ if(!descriptorAuthority?.descriptor
1743
+ ||!/^[a-f0-9]{64}$/u.test(descriptorAuthority.descriptorSha256)){
1744
+ fail('Canonical app descriptor authority is required to issue a release receipt.');
1745
+ }
1746
+ const receipt={
1747
+ schemaVersion:1,
1748
+ kind:'arcane-app-release-verification',
1749
+ generation:randomBytes(16).toString('hex'),
1750
+ canonicalLocation:state.canonical,
1751
+ builder:release.builder,
1752
+ app:immutableJsonCopy(release.app),
1753
+ policySha256:release.policySha256,
1754
+ files:immutableJsonCopy(release.files),
1755
+ fileCount:release.fileCount,
1756
+ totalBytes:release.totalBytes,
1757
+ contentSha256:release.contentSha256
1758
+ };
1759
+ Object.defineProperty(receipt,'identities',{
1760
+ value:Object.freeze([...identities]),
1761
+ enumerable:false,
1762
+ writable:false,
1763
+ configurable:false
1764
+ });
1765
+ Object.freeze(receipt);
1766
+ issuedAppReleaseReceipts.set(receipt,Object.freeze({
1767
+ canonicalLocation:state.canonical,
1768
+ rootIdentity:state.rootIdentity,
1769
+ identities:receipt.identities,
1770
+ packageBinding,
1771
+ descriptorAuthority:Object.freeze({
1772
+ descriptor:descriptorAuthority.descriptor,
1773
+ descriptorSha256:descriptorAuthority.descriptorSha256,
1774
+ source:descriptorAuthority.source
1775
+ })
1776
+ }));
1777
+ return receipt;
1778
+ }
1779
+
1780
+ async function authenticateAppReleaseReceiptState(receipt,{
1781
+ releaseRoot,
1782
+ expectedPackageConfig,
1783
+ signal
1784
+ }={}){
1785
+ const state=issuedAppReleaseReceipts.get(receipt);
1786
+ if(!state)fail('App release receipt was not issued by this SDK process.');
1787
+ if(typeof releaseRoot!=='string'||!releaseRoot.trim())fail('releaseRoot is required to authenticate an app release receipt.');
1788
+ const requested=path.resolve(releaseRoot);
1789
+ const canonical=await realpath(requested);
1790
+ if(canonical!==state.canonicalLocation||receipt.canonicalLocation!==canonical){
1791
+ fail('App release receipt belongs to a different release location.');
1792
+ }
1793
+ const rootInfo=await lstat(canonical,{bigint:true});
1794
+ if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()
1795
+ ||!identityMatches(rootInfo,state.rootIdentity)){
1796
+ fail('App release root changed after its receipt was issued.');
1797
+ }
1798
+ if(expectedPackageConfig!==undefined
1799
+ &&JSON.stringify(appReleasePackageBinding(expectedPackageConfig))
1800
+ !==JSON.stringify(state.packageBinding)){
1801
+ fail('App release receipt belongs to a different authored package policy.');
1802
+ }
1803
+ await assertArtifactState(canonical,state.identities,{signal});
1804
+ return state;
1805
+ }
1806
+
1807
+ export async function authenticateAppReleaseReceipt(receipt,options={}){
1808
+ await authenticateAppReleaseReceiptState(receipt,options);
1809
+ return receipt;
1810
+ }
1811
+
1812
+ export async function authenticateAppReleaseAuthority(receipt,{
1813
+ releaseRoot,
1814
+ expectedPackageConfig,
1815
+ expectedDescriptor,
1816
+ signal
1817
+ }={}){
1818
+ const state=await authenticateAppReleaseReceiptState(receipt,{
1819
+ releaseRoot,
1820
+ expectedPackageConfig,
1821
+ signal
1822
+ });
1823
+ const authority=state.descriptorAuthority;
1824
+ if(!authority?.descriptor||!/^[a-f0-9]{64}$/u.test(authority.descriptorSha256)){
1825
+ fail('App release receipt is missing its canonical descriptor authority.');
1826
+ }
1827
+ if(expectedDescriptor!==undefined){
1828
+ const contracts=await appDescriptorContracts();
1829
+ if(contracts.appDescriptorSha256(expectedDescriptor)!==authority.descriptorSha256){
1830
+ fail('App release receipt belongs to a different canonical app descriptor.');
1831
+ }
1832
+ }
1833
+ return Object.freeze({
1834
+ receipt,
1835
+ descriptor:authority.descriptor,
1836
+ descriptorSha256:authority.descriptorSha256,
1837
+ source:authority.source
1838
+ });
1839
+ }
1840
+
1841
+ export async function readVerifiedAppReleaseFile(receipt,{
1842
+ releaseRoot,
1843
+ relativePath,
1844
+ signal
1845
+ }={}){
1846
+ throwIfAborted(signal);
1847
+ const state=issuedAppReleaseReceipts.get(receipt);
1848
+ if(!state)fail('App release receipt was not issued by this SDK process.');
1849
+ if(typeof releaseRoot!=='string'||!releaseRoot.trim()){
1850
+ fail('releaseRoot is required to read a verified app release file.');
1851
+ }
1852
+ const requested=path.resolve(releaseRoot);
1853
+ const canonical=await realpath(requested);
1854
+ if(canonical!==state.canonicalLocation||receipt.canonicalLocation!==canonical){
1855
+ fail('App release receipt belongs to a different release location.');
1856
+ }
1857
+ const rootInfo=await lstat(canonical,{bigint:true});
1858
+ if(rootInfo.isSymbolicLink()||!rootInfo.isDirectory()
1859
+ ||!identityMatches(rootInfo,state.rootIdentity)){
1860
+ fail('App release root changed after its receipt was issued.');
1861
+ }
1862
+
1863
+ const normalized=normalizeRelativePath(relativePath,'verified app release path');
1864
+ const file=receipt.files.find(candidate=>pathKey(candidate.path)===pathKey(normalized));
1865
+ if(!file)fail(`Path is not in the verified app release inventory: ${normalized}.`);
1866
+ if(file.bytes>MAX_VERIFIED_APP_FILE_BYTES){
1867
+ fail(
1868
+ `Verified browser file exceeds the ${MAX_VERIFIED_APP_FILE_BYTES}-byte development serving limit: ${file.path}.`,
1869
+ 'ARCANE_POLICY_DENIED'
1870
+ );
1871
+ }
1872
+ const identity=state.identities.find(candidate=>
1873
+ pathKey(candidate.path)===pathKey(file.path)
1874
+ );
1875
+ if(!identity)fail(`Verified app release identity is missing for ${file.path}.`);
1876
+ const filePath=resolveInside(canonical,file.path,'verified app release path');
1877
+ const opened=await openStableRegularFile(filePath,`verified app release file ${file.path}`,identity);
1878
+ try{
1879
+ throwIfAborted(signal);
1880
+ const bytes=await opened.handle.readFile();
1881
+ throwIfAborted(signal);
1882
+ const after=await opened.handle.stat({bigint:true});
1883
+ if(!identityMatches(after,opened.identity)||bytes.length!==file.bytes){
1884
+ fail(`Verified app release file changed while it was being read: ${file.path}.`);
1885
+ }
1886
+ const digest=createHash('sha256').update(bytes).digest('hex');
1887
+ if(digest!==file.sha256){
1888
+ fail(`Verified app release file hash changed: ${file.path}.`);
1889
+ }
1890
+ const current=await lstat(filePath,{bigint:true});
1891
+ if(current.isSymbolicLink()||!current.isFile()||!identityMatches(current,identity)){
1892
+ fail(`Verified app release path changed while it was being read: ${file.path}.`);
1893
+ }
1894
+ const canonicalFile=await realpath(filePath);
1895
+ if(!isInside(canonical,canonicalFile)){
1896
+ fail(`Verified app release path left its release root: ${file.path}.`);
1897
+ }
1898
+ return bytes;
1899
+ }finally{
1900
+ await opened.handle.close();
1901
+ }
1902
+ }
1903
+
1904
+ function normalizedRoutePolicy(route){
1905
+ return {
1906
+ source:route.source,
1907
+ destination:route.destination,
1908
+ include:[...route.include].sort(compareText),
1909
+ exclude:[...route.exclude].sort(compareText)
1910
+ };
1911
+ }
1912
+
1913
+ async function packagePolicySha256(context,{signal}={}){
1914
+ throwIfAborted(signal);
1915
+ const {config,rootConfig,appRoot}=context;
1916
+ let adapter=null;
1917
+
1918
+ if(config.adapter){
1919
+ const adapterPath=resolveInside(appRoot,config.adapter,`${config.id} adapter`);
1920
+ await assertNoLinks(appRoot,adapterPath,`${config.id} adapter`);
1921
+ adapter={
1922
+ path:config.adapter,
1923
+ sha256:await sha256(adapterPath,{signal})
1924
+ };
1925
+ }
1926
+
1927
+ const shared=[...config.shared]
1928
+ .sort(compareText)
1929
+ .map(id=>({
1930
+ id,
1931
+ routes:rootConfig.sharedPayloads[id]
1932
+ .map(normalizedRoutePolicy)
1933
+ .sort((left,right)=>compareText(JSON.stringify(left),JSON.stringify(right)))
1934
+ }));
1935
+ const policy={
1936
+ strategy:config.strategy,
1937
+ security:config.security,
1938
+ localAIModelPolicy:{...config.localAIModelPolicy},
1939
+ include:[...config.include].sort(compareText),
1940
+ exclude:[...config.exclude].sort(compareText),
1941
+ shared,
1942
+ adapter
1943
+ };
1944
+
1945
+ return createHash('sha256')
1946
+ .update(JSON.stringify(policy))
1947
+ .digest('hex');
1948
+ }
1949
+
1950
+ async function writeReleaseManifest(root,context,version,{signal,onEvent}={}){
1951
+ const {config}=context;
1952
+ const inventory=await inventoryEntries(root,{signal,onEvent});
1953
+ const files=inventory.entries;
1954
+ const totalBytes=files.reduce((total,file)=>total+file.bytes,0);
1955
+ const contentSha256=createHash('sha256')
1956
+ .update(JSON.stringify(files))
1957
+ .digest('hex');
1958
+ const release={
1959
+ schemaVersion:1,
1960
+ builder:PACKAGER_VERSION,
1961
+ app:{
1962
+ id:config.id,
1963
+ displayName:config.displayName,
1964
+ version,
1965
+ entry:config.entry,
1966
+ start:`./apps/${config.id}/${config.entry}`,
1967
+ security:config.security,
1968
+ localAIModelPolicy:{...config.localAIModelPolicy}
1969
+ },
1970
+ policySha256:await packagePolicySha256(context,{signal}),
1971
+ fileCount:files.length,
1972
+ totalBytes,
1973
+ contentSha256,
1974
+ files
1975
+ };
1976
+
1977
+ const manifestPath=path.join(root,RELEASE_MANIFEST_NAME);
1978
+ await writeFile(
1979
+ manifestPath,
1980
+ `${JSON.stringify(release,null,2)}\n`,
1981
+ {encoding:'utf8',flag:'wx'}
1982
+ );
1983
+ const manifest=await openStableRegularFile(manifestPath,RELEASE_MANIFEST_NAME);
1984
+ await manifest.handle.close();
1985
+ return {
1986
+ release,
1987
+ identities:Object.freeze([
1988
+ ...inventory.identities,
1989
+ Object.freeze({path:RELEASE_MANIFEST_NAME,...manifest.identity})
1990
+ ])
1991
+ };
1992
+ }
1993
+
1994
+ function expectedReleaseApp(context,version){
1995
+ const {config}=context;
1996
+ return {
1997
+ id:config.id,
1998
+ displayName:config.displayName,
1999
+ version,
2000
+ entry:config.entry,
2001
+ start:`./apps/${config.id}/${config.entry}`,
2002
+ security:config.security,
2003
+ localAIModelPolicy:{...config.localAIModelPolicy}
2004
+ };
2005
+ }
2006
+
2007
+ async function verifyFreshStaticRelease(root,context,version,releaseState,{signal}={}){
2008
+ throwIfAborted(signal);
2009
+ const {config}=context;
2010
+ const expectedApp=expectedReleaseApp(context,version);
2011
+ const release=releaseState.release;
2012
+
2013
+ if(release?.schemaVersion!==1||release?.builder!==PACKAGER_VERSION
2014
+ ||JSON.stringify(release?.app)!==JSON.stringify(expectedApp)
2015
+ ||!Array.isArray(release?.files)
2016
+ ||release.fileCount!==release.files.length){
2017
+ fail(`${config.id}/${RELEASE_MANIFEST_NAME} identity is invalid.`);
2018
+ }
2019
+
2020
+ // This staging tree is owned by this operation and no adapter has run. The
2021
+ // release inventory was produced from the final bytes immediately before
2022
+ // this check, so reuse that receipt instead of hashing every file twice.
2023
+ const rootIndex=path.join(root,'index.html');
2024
+ const entry=resolveInside(root,expectedApp.start.slice(2),'package entry');
2025
+ const [indexInfo,entryInfo,manifestInfo]=await Promise.all([
2026
+ lstat(rootIndex),
2027
+ lstat(entry),
2028
+ lstat(path.join(root,RELEASE_MANIFEST_NAME))
2029
+ ]);
2030
+
2031
+ if(!indexInfo.isFile()||indexInfo.isSymbolicLink()
2032
+ ||!entryInfo.isFile()||entryInfo.isSymbolicLink()
2033
+ ||!manifestInfo.isFile()||manifestInfo.isSymbolicLink()){
2034
+ fail(`Package entry files for ${config.id} are invalid.`);
2035
+ }
2036
+
2037
+ return releaseState;
2038
+ }
2039
+
2040
+ async function verifyGenericRelease(root,context,version,{signal,onEvent}={}){
2041
+ const {config}=context;
2042
+ const manifestDocument=await readJsonDocument(
2043
+ path.join(root,RELEASE_MANIFEST_NAME),
2044
+ `${config.id}/${RELEASE_MANIFEST_NAME}`
2045
+ );
2046
+ const release=manifestDocument.value;
2047
+ const expectedApp=expectedReleaseApp(context,version);
2048
+
2049
+ if(release?.schemaVersion!==1||release?.builder!==PACKAGER_VERSION
2050
+ ||JSON.stringify(release?.app)!==JSON.stringify(expectedApp)
2051
+ ||release?.policySha256!==await packagePolicySha256(context,{signal})
2052
+ ||!Array.isArray(release?.files)){
2053
+ fail(`${config.id}/${RELEASE_MANIFEST_NAME} identity is invalid.`);
2054
+ }
2055
+
2056
+ const inventory=await inventoryEntries(root,{signal,onEvent});
2057
+ const actualFiles=inventory.entries;
2058
+ const totalBytes=actualFiles.reduce((total,file)=>total+file.bytes,0);
2059
+ const contentSha256=createHash('sha256')
2060
+ .update(JSON.stringify(actualFiles))
2061
+ .digest('hex');
2062
+
2063
+ if(release.fileCount!==actualFiles.length
2064
+ ||release.totalBytes!==totalBytes
2065
+ ||release.contentSha256!==contentSha256
2066
+ ||JSON.stringify(release.files)!==JSON.stringify(actualFiles)){
2067
+ fail(`${config.id}/${RELEASE_MANIFEST_NAME} does not match the package tree.`);
2068
+ }
2069
+
2070
+ const rootIndex=path.join(root,'index.html');
2071
+ const entry=resolveInside(root,expectedApp.start.slice(2),'package entry');
2072
+ const [indexInfo,entryInfo]=await Promise.all([lstat(rootIndex),lstat(entry)]);
2073
+
2074
+ if(!indexInfo.isFile()||indexInfo.isSymbolicLink()
2075
+ ||!entryInfo.isFile()||entryInfo.isSymbolicLink()){
2076
+ fail(`Package entry files for ${config.id} are invalid.`);
2077
+ }
2078
+
2079
+ return {
2080
+ release,
2081
+ identities:Object.freeze([
2082
+ ...inventory.identities,
2083
+ Object.freeze({path:RELEASE_MANIFEST_NAME,...manifestDocument.identity})
2084
+ ])
2085
+ };
2086
+ }
2087
+
2088
+ async function loadAdapter(context){
2089
+ if(context.config.strategy!=='adapter'){
2090
+ return null;
2091
+ }
2092
+
2093
+ const adapterPath=resolveInside(
2094
+ context.appRoot,
2095
+ context.config.adapter,
2096
+ `${context.config.id} adapter`
2097
+ );
2098
+ await assertNoLinks(context.appRoot,adapterPath,`${context.config.id} adapter`);
2099
+ const details=await stat(adapterPath);
2100
+
2101
+ if(!details.isFile()){
2102
+ fail(`${context.config.id} adapter is not a regular file.`);
2103
+ }
2104
+
2105
+ const adapterBytes=await readFile(adapterPath);
2106
+
2107
+ if(adapterBytes.includes(0x0d)){
2108
+ fail(`${context.config.id} adapter must use canonical LF line endings.`);
2109
+ }
2110
+
2111
+ const module=await import(`${pathToFileURL(adapterPath).href}?mtime=${details.mtimeMs}`);
2112
+
2113
+ if(typeof module.buildArcanePackage!=='function'
2114
+ ||typeof module.verifyArcanePackage!=='function'){
2115
+ fail(`${context.config.id} adapter must export buildArcanePackage and verifyArcanePackage.`);
2116
+ }
2117
+
2118
+ return module;
2119
+ }
2120
+
2121
+ async function verifyBuiltPackage(context,outputRoot,version,adapter,{signal,onEvent}={}){
2122
+ throwIfAborted(signal);
2123
+ if(adapter){
2124
+ await adapter.verifyArcanePackage({
2125
+ workspaceRoot:context.workspaceRoot,
2126
+ appRoot:context.appRoot,
2127
+ outputRoot,
2128
+ config:context.config,
2129
+ version,
2130
+ signal,
2131
+ onEvent
2132
+ });
2133
+ }
2134
+
2135
+ return verifyGenericRelease(outputRoot,context,version,{signal,onEvent});
2136
+ }
2137
+
2138
+ async function writeAppVersion(context,version){
2139
+ parseSemver(version);
2140
+ const raw=await readJson(context.config.configPath,context.config.configPath);
2141
+ raw.version=version;
2142
+ const temporary=`${context.config.configPath}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
2143
+ const backup=`${context.config.configPath}.bak-${process.pid}-${randomBytes(4).toString('hex')}`;
2144
+ await writeFile(temporary,`${JSON.stringify(raw,null,2)}\n`,'utf8');
2145
+
2146
+ let originalMoved=false;
2147
+ let replacementInstalled=false;
2148
+
2149
+ try{
2150
+ await rename(context.config.configPath,backup);
2151
+ originalMoved=true;
2152
+ await rename(temporary,context.config.configPath);
2153
+ replacementInstalled=true;
2154
+ }catch(error){
2155
+ await rm(temporary,{force:true});
2156
+
2157
+ if(originalMoved&&!replacementInstalled){
2158
+ try{
2159
+ await rename(backup,context.config.configPath);
2160
+ }catch{
2161
+ // Preserve the original error; the backup path remains recoverable.
2162
+ }
2163
+ }
2164
+
2165
+ throw error;
2166
+ }
2167
+
2168
+ await rm(backup,{force:true}).catch(()=>{});
2169
+ }
2170
+
2171
+ function resolveTargetVersion(current,{bump,exactVersion,preid}={}){
2172
+ if(bump&&exactVersion){
2173
+ fail('Choose either a semantic version bump or an exact version, not both.');
2174
+ }
2175
+
2176
+ if(exactVersion!==undefined){
2177
+ parseSemver(exactVersion);
2178
+
2179
+ if(exactVersion===current){
2180
+ fail(`Version is already ${current}.`);
2181
+ }
2182
+
2183
+ return exactVersion;
2184
+ }
2185
+
2186
+ return bump?incrementSemver(current,bump,preid):current;
2187
+ }
2188
+
2189
+ async function acquirePackageLock(distRoot,appId){
2190
+ const lockPath=path.join(distRoot,`.arcane-packager-${appId}.lock`);
2191
+ let handle;
2192
+
2193
+ try{
2194
+ handle=await open(lockPath,'wx');
2195
+ await handle.writeFile(`${JSON.stringify({pid:process.pid,app:appId})}\n`,'utf8');
2196
+ }catch(error){
2197
+ if(handle){
2198
+ await handle.close().catch(()=>{});
2199
+ await rm(lockPath,{force:true}).catch(()=>{});
2200
+ }
2201
+
2202
+ if(error?.code==='EEXIST'){
2203
+ fail(`Another package operation for ${appId} is already running. If no process is active, remove the stale lock at ${lockPath}.`);
2204
+ }
2205
+
2206
+ throw error;
2207
+ }
2208
+
2209
+ let released=false;
2210
+
2211
+ return async()=>{
2212
+ if(released){
2213
+ return;
2214
+ }
2215
+
2216
+ released=true;
2217
+ const cleanupErrors=[];
2218
+
2219
+ try{
2220
+ await handle.close();
2221
+ }catch(error){
2222
+ cleanupErrors.push(`close failed: ${error.message}`);
2223
+ }
2224
+
2225
+ try{
2226
+ await rm(lockPath,{force:true});
2227
+ }catch(error){
2228
+ cleanupErrors.push(`remove failed: ${error.message}`);
2229
+ }
2230
+
2231
+ if(cleanupErrors.length){
2232
+ console.error(
2233
+ `Arcane packager completed but could not fully clean ${lockPath} (${cleanupErrors.join('; ')}). Remove the stale lock before the next operation.`
2234
+ );
2235
+ }
2236
+ };
2237
+ }
2238
+
2239
+ async function acquireOperationLock(workspaceRoot,appId){
2240
+ const resolvedWorkspace=normalizeWorkspaceRoot(workspaceRoot);
2241
+
2242
+ if(typeof appId!=='string'||!APP_ID_PATTERN.test(appId)){
2243
+ fail(`Invalid app id: ${String(appId)}`);
2244
+ }
2245
+
2246
+ const rootConfig=await loadRootConfig(resolvedWorkspace);
2247
+ const distRoot=path.join(resolvedWorkspace,rootConfig.distRoot);
2248
+ await assertSafeDistBoundary(resolvedWorkspace,distRoot,{create:true});
2249
+ return acquirePackageLock(distRoot,appId);
2250
+ }
2251
+
2252
+ async function readDistVersion(outputRoot){
2253
+ try{
2254
+ const release=await readJson(path.join(outputRoot,RELEASE_MANIFEST_NAME));
2255
+ return typeof release?.app?.version==='string'?release.app.version:null;
2256
+ }catch{
2257
+ return null;
2258
+ }
2259
+ }
2260
+
2261
+ export async function discoverApps({workspaceRoot:requestedWorkspaceRoot}){
2262
+ const workspaceRoot=normalizeWorkspaceRoot(requestedWorkspaceRoot);
2263
+ const rootConfig=await loadRootConfig(workspaceRoot);
2264
+ const appsRoot=path.join(workspaceRoot,rootConfig.appsRoot);
2265
+ const entries=await readdir(appsRoot,{withFileTypes:true});
2266
+ const apps=[];
2267
+
2268
+ for(const entry of entries.sort((left,right)=>compareText(left.name,right.name))){
2269
+ if(!APP_ID_PATTERN.test(entry.name)||(entry.isFile()&&!entry.isSymbolicLink())){
2270
+ continue;
2271
+ }
2272
+
2273
+ if(entry.isSymbolicLink()){
2274
+ apps.push({
2275
+ id:entry.name,
2276
+ displayName:entry.name,
2277
+ configured:false,
2278
+ status:'unsafe-link',
2279
+ version:null,
2280
+ distVersion:null
2281
+ });
2282
+ continue;
2283
+ }
2284
+
2285
+ if(!entry.isDirectory()){
2286
+ continue;
2287
+ }
2288
+
2289
+ const configPath=path.join(appsRoot,entry.name,APP_CONFIG_NAME);
2290
+
2291
+ try{
2292
+ const context=await getAppContext({workspaceRoot,appId:entry.name});
2293
+ apps.push({
2294
+ id:entry.name,
2295
+ displayName:context.config.displayName,
2296
+ configured:true,
2297
+ status:'ready',
2298
+ version:context.config.version,
2299
+ distVersion:await readDistVersion(context.outputRoot),
2300
+ strategy:context.config.strategy,
2301
+ entry:context.config.entry,
2302
+ output:path.relative(workspaceRoot,context.outputRoot).replaceAll('\\','/')
2303
+ });
2304
+ }catch(error){
2305
+ let configured=true;
2306
+
2307
+ try{
2308
+ await lstat(configPath);
2309
+ }catch{
2310
+ configured=false;
2311
+ }
2312
+
2313
+ let displayName=entry.name;
2314
+
2315
+ try{
2316
+ const manifest=await readJson(path.join(appsRoot,entry.name,'manifest.json'));
2317
+ if(typeof manifest?.name==='string'&&manifest.name.trim()){
2318
+ displayName=manifest.name.trim();
2319
+ }
2320
+ }catch{
2321
+ // An unconfigured app can still be listed without a PWA manifest.
2322
+ }
2323
+
2324
+ apps.push({
2325
+ id:entry.name,
2326
+ displayName,
2327
+ configured,
2328
+ status:configured?'invalid':'unconfigured',
2329
+ version:null,
2330
+ distVersion:null,
2331
+ error:configured?error.message:undefined
2332
+ });
2333
+ }
2334
+ }
2335
+
2336
+ return apps;
2337
+ }
2338
+
2339
+ export async function inspectApp({workspaceRoot,appId}){
2340
+ const context=await getAppContext({workspaceRoot,appId});
2341
+ const files=await collectPackageFiles(context);
2342
+ const totalBytes=files.reduce((total,file)=>total+file.bytes,0);
2343
+ const largestFiles=[...files]
2344
+ .sort((left,right)=>right.bytes-left.bytes||compareText(left.destination,right.destination))
2345
+ .slice(0,10)
2346
+ .map(file=>({path:file.destination,bytes:file.bytes}));
2347
+
2348
+ return {
2349
+ id:context.config.id,
2350
+ displayName:context.config.displayName,
2351
+ version:context.config.version,
2352
+ distVersion:await readDistVersion(context.outputRoot),
2353
+ strategy:context.config.strategy,
2354
+ entry:context.config.entry,
2355
+ output:path.relative(context.workspaceRoot,context.outputRoot).replaceAll('\\','/'),
2356
+ include:[...context.config.include],
2357
+ exclude:[...context.config.exclude],
2358
+ shared:[...context.config.shared],
2359
+ adapter:context.config.adapter,
2360
+ baseFileCount:files.length,
2361
+ baseBytes:totalBytes,
2362
+ largestFiles,
2363
+ note:context.config.strategy==='adapter'
2364
+ ?'Counts cover the static base; the adapter can add generated public files.'
2365
+ :undefined
2366
+ };
2367
+ }
2368
+
2369
+ async function packageAppUnlocked({
2370
+ workspaceRoot,
2371
+ appId,
2372
+ bump,
2373
+ preid,
2374
+ exactVersion,
2375
+ dryRun=false,
2376
+ context:preparedContext,
2377
+ sharedPayloadSnapshot,
2378
+ authenticatedSharedPayloadState,
2379
+ signal,
2380
+ onEvent,
2381
+ validateSourceState
2382
+ }){
2383
+ throwIfAborted(signal);
2384
+ if(validateSourceState!==undefined&&typeof validateSourceState!=='function'){
2385
+ fail('validateSourceState must be a function when provided.');
2386
+ }
2387
+ const context=preparedContext??await getAppContext({
2388
+ workspaceRoot,
2389
+ appId,
2390
+ bindDescriptorAuthority:true,
2391
+ signal
2392
+ });
2393
+ const currentVersion=context.config.version;
2394
+ const version=resolveTargetVersion(currentVersion,{bump,exactVersion,preid});
2395
+ const files=await collectPackageFiles(context,{
2396
+ signal,
2397
+ sharedPayloadState:authenticatedSharedPayloadState
2398
+ });
2399
+ const preview={
2400
+ app:appId,
2401
+ currentVersion,
2402
+ version,
2403
+ bump:bump??null,
2404
+ dryRun:Boolean(dryRun),
2405
+ output:path.relative(context.workspaceRoot,context.outputRoot).replaceAll('\\','/'),
2406
+ baseFileCount:files.length,
2407
+ baseBytes:files.reduce((total,file)=>total+file.bytes,0),
2408
+ strategy:context.config.strategy
2409
+ };
2410
+
2411
+ if(dryRun){
2412
+ throwIfAborted(signal);
2413
+ return preview;
2414
+ }
2415
+
2416
+ const token=`${process.pid}-${Date.now()}-${randomBytes(4).toString('hex')}`;
2417
+ const staging=resolveInside(context.distRoot,`.arcane-packager-${appId}-${token}`,'staging output');
2418
+ const stagingTemporary=`${staging}.tmp`;
2419
+ const backup=resolveInside(context.distRoot,`.arcane-packager-${appId}-backup-${token}`,'backup output');
2420
+ const failedOutput=resolveInside(context.distRoot,`.arcane-packager-${appId}-failed-${token}`,'failed output');
2421
+ let adapter=null;
2422
+ let movedExisting=false;
2423
+ let promoted=false;
2424
+ let operationSucceeded=false;
2425
+ let rollbackRestored=false;
2426
+
2427
+ try{
2428
+ await rm(staging,{recursive:true,force:true});
2429
+ await rm(stagingTemporary,{recursive:true,force:true});
2430
+ await rm(backup,{recursive:true,force:true}).catch(()=>{});
2431
+ await rm(failedOutput,{recursive:true,force:true}).catch(()=>{});
2432
+ adapter=await loadAdapter(context);
2433
+ throwIfAborted(signal);
2434
+
2435
+ if(adapter){
2436
+ let prepared=false;
2437
+ await adapter.buildArcanePackage({
2438
+ workspaceRoot:context.workspaceRoot,
2439
+ appRoot:context.appRoot,
2440
+ outputRoot:staging,
2441
+ config:context.config,
2442
+ version,
2443
+ signal,
2444
+ onEvent,
2445
+ deferFinalVerification:true,
2446
+ prepareBase:async outputRoot=>{
2447
+ if(prepared){
2448
+ fail(`${appId} adapter requested its base payload more than once.`);
2449
+ }
2450
+
2451
+ prepared=true;
2452
+ const requestedRoot=path.resolve(outputRoot);
2453
+
2454
+ if(requestedRoot!==path.resolve(staging)
2455
+ &&requestedRoot!==path.resolve(stagingTemporary)){
2456
+ fail(`${appId} adapter requested its base payload outside its assigned staging roots.`);
2457
+ }
2458
+
2459
+ await materializeBasePackage(context,outputRoot,files,{signal,onEvent});
2460
+ }
2461
+ });
2462
+
2463
+ if(!prepared){
2464
+ fail(`${appId} adapter did not materialize the configured public base payload.`);
2465
+ }
2466
+ }else{
2467
+ await materializeBasePackage(context,staging,files,{signal,onEvent});
2468
+ }
2469
+
2470
+ throwIfAborted(signal);
2471
+ const releaseState=await writeReleaseManifest(staging,context,version,{signal,onEvent});
2472
+ const verifiedRelease=adapter
2473
+ ?await verifyBuiltPackage(context,staging,version,adapter,{signal,onEvent})
2474
+ :await verifyFreshStaticRelease(staging,context,version,releaseState,{signal});
2475
+
2476
+ throwIfAborted(signal);
2477
+ if(validateSourceState){
2478
+ const validation=await validateSourceState({signal});
2479
+ await assertValidatedDescriptorAuthority(validation,context.descriptorAuthority);
2480
+ }
2481
+ await assertAppDescriptorAuthorityCurrent(context,{signal});
2482
+ if(sharedPayloadSnapshot!==undefined){
2483
+ await authenticateSharedPayloadSnapshotState(sharedPayloadSnapshot,{
2484
+ workspaceRoot:context.workspaceRoot,
2485
+ sharedPayloadIds:context.config.shared,
2486
+ signal
2487
+ });
2488
+ }
2489
+ await assertArtifactState(staging,verifiedRelease.identities,{signal});
2490
+ throwIfAborted(signal);
2491
+
2492
+ try{
2493
+ await lstat(context.outputRoot);
2494
+ await renamePackageDirectory(context.outputRoot,backup);
2495
+ movedExisting=true;
2496
+ }catch(error){
2497
+ if(error?.code!=='ENOENT'){
2498
+ throw error;
2499
+ }
2500
+ }
2501
+
2502
+ throwIfAborted(signal);
2503
+ await renamePackageDirectory(staging,context.outputRoot);
2504
+ promoted=true;
2505
+
2506
+ throwIfAborted(signal);
2507
+ const receipt=await issueAppReleaseReceipt(
2508
+ context.outputRoot,
2509
+ verifiedRelease.release,
2510
+ verifiedRelease.identities,
2511
+ {
2512
+ signal,
2513
+ packageConfig:{...context.config,version},
2514
+ descriptorAuthority:context.descriptorAuthority
2515
+ }
2516
+ );
2517
+
2518
+ if(version!==currentVersion){
2519
+ await writeAppVersion(context,version);
2520
+ }
2521
+
2522
+ operationSucceeded=true;
2523
+ return {
2524
+ ...preview,
2525
+ dryRun:false,
2526
+ fileCount:verifiedRelease.release.fileCount,
2527
+ totalBytes:verifiedRelease.release.totalBytes,
2528
+ contentSha256:verifiedRelease.release.contentSha256,
2529
+ receipt
2530
+ };
2531
+ }catch(error){
2532
+ const rollbackErrors=[];
2533
+ let targetVacated=!promoted;
2534
+
2535
+ if(promoted){
2536
+ try{
2537
+ await renamePackageDirectory(context.outputRoot,failedOutput);
2538
+ targetVacated=true;
2539
+ }catch(moveError){
2540
+ targetVacated=false;
2541
+ rollbackErrors.push(`could not move the failed package aside: ${moveError.message}`);
2542
+ }
2543
+ }
2544
+
2545
+ if(movedExisting&&targetVacated){
2546
+ try{
2547
+ await renamePackageDirectory(backup,context.outputRoot);
2548
+ rollbackRestored=true;
2549
+ }catch(restoreError){
2550
+ rollbackErrors.push(`could not restore the previous package from ${backup}: ${restoreError.message}`);
2551
+ }
2552
+ }
2553
+
2554
+ if(rollbackErrors.length){
2555
+ error.message+=` Rollback warning: ${rollbackErrors.join('; ')}. Preserve ${backup} until manually recovered.`;
2556
+ }
2557
+
2558
+ throw error;
2559
+ }finally{
2560
+ await rm(staging,{recursive:true,force:true}).catch(()=>{});
2561
+ await rm(stagingTemporary,{recursive:true,force:true}).catch(()=>{});
2562
+ await rm(failedOutput,{recursive:true,force:true}).catch(()=>{});
2563
+
2564
+ if(operationSucceeded||rollbackRestored||!movedExisting){
2565
+ await rm(backup,{recursive:true,force:true}).catch(()=>{});
2566
+ }
2567
+ }
2568
+ }
2569
+
2570
+ export async function packageApp(options){
2571
+ throwIfAborted(options?.signal);
2572
+ const context=await getAppContext({
2573
+ workspaceRoot:options?.workspaceRoot,
2574
+ appId:options?.appId,
2575
+ bindDescriptorAuthority:true,
2576
+ signal:options?.signal
2577
+ });
2578
+ throwIfAborted(options?.signal);
2579
+ const authenticatedSharedPayloadState=options?.sharedPayloadSnapshot===undefined
2580
+ ?null
2581
+ :await authenticateSharedPayloadSnapshotState(options.sharedPayloadSnapshot,{
2582
+ workspaceRoot:context.workspaceRoot,
2583
+ sharedPayloadIds:context.config.shared,
2584
+ signal:options?.signal
2585
+ });
2586
+ if(options?.dryRun){
2587
+ return packageAppUnlocked({...options,context,authenticatedSharedPayloadState});
2588
+ }
2589
+
2590
+ await assertSafeDistBoundary(context.workspaceRoot,context.distRoot,{create:true});
2591
+ const releaseLock=await acquirePackageLock(context.distRoot,options?.appId);
2592
+
2593
+ try{
2594
+ return await packageAppUnlocked({...options,context,authenticatedSharedPayloadState});
2595
+ }finally{
2596
+ await releaseLock();
2597
+ }
2598
+ }
2599
+
2600
+ export async function verifyApp({workspaceRoot,appId,signal,onEvent}){
2601
+ throwIfAborted(signal);
2602
+ const context=await getAppContext({
2603
+ workspaceRoot,
2604
+ appId,
2605
+ bindDescriptorAuthority:true,
2606
+ signal
2607
+ });
2608
+ const adapter=await loadAdapter(context);
2609
+ const releaseState=await verifyBuiltPackage(
2610
+ context,
2611
+ context.outputRoot,
2612
+ context.config.version,
2613
+ adapter,
2614
+ {signal,onEvent}
2615
+ );
2616
+ const release=releaseState.release;
2617
+ const receipt=await issueAppReleaseReceipt(
2618
+ context.outputRoot,
2619
+ release,
2620
+ releaseState.identities,
2621
+ {
2622
+ signal,
2623
+ packageConfig:context.config,
2624
+ descriptorAuthority:context.descriptorAuthority
2625
+ }
2626
+ );
2627
+
2628
+ return {
2629
+ app:appId,
2630
+ // The verified manifest is already bound to this exact configured
2631
+ // version; do not couple the public verify result to a nested app
2632
+ // representation used by a particular schema generation.
2633
+ version:context.config.version,
2634
+ output:path.relative(context.workspaceRoot,context.outputRoot).replaceAll('\\','/'),
2635
+ fileCount:release.fileCount,
2636
+ totalBytes:release.totalBytes,
2637
+ contentSha256:release.contentSha256,
2638
+ verified:true,
2639
+ receipt
2640
+ };
2641
+ }
2642
+
2643
+ async function bumpVersionUnlocked({
2644
+ workspaceRoot,
2645
+ appId,
2646
+ bump,
2647
+ preid,
2648
+ exactVersion,
2649
+ dryRun=false
2650
+ }){
2651
+ const context=await getAppContext({workspaceRoot,appId});
2652
+ const currentVersion=context.config.version;
2653
+ const version=resolveTargetVersion(currentVersion,{bump,exactVersion,preid});
2654
+
2655
+ if(version===currentVersion){
2656
+ fail('A bump level or exact version is required.');
2657
+ }
2658
+
2659
+ if(!dryRun){
2660
+ await writeAppVersion(context,version);
2661
+ }
2662
+
2663
+ return {
2664
+ app:appId,
2665
+ currentVersion,
2666
+ version,
2667
+ bump:bump??null,
2668
+ dryRun:Boolean(dryRun)
2669
+ };
2670
+ }
2671
+
2672
+ export async function bumpVersion(options){
2673
+ if(options?.dryRun){
2674
+ return bumpVersionUnlocked(options);
2675
+ }
2676
+
2677
+ await getAppContext({
2678
+ workspaceRoot:options?.workspaceRoot,
2679
+ appId:options?.appId
2680
+ });
2681
+ const releaseLock=await acquireOperationLock(
2682
+ options?.workspaceRoot,
2683
+ options?.appId
2684
+ );
2685
+
2686
+ try{
2687
+ return await bumpVersionUnlocked(options);
2688
+ }finally{
2689
+ await releaseLock();
2690
+ }
2691
+ }