arcane-os 0.1.0-dev.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +154 -0
- package/COMMERCIAL-LICENSE.md +13 -0
- package/LICENSE +661 -0
- package/NOTICE +70 -0
- package/README.md +448 -0
- package/bin/arcane-test.mjs +741 -0
- package/bin/arcane.mjs +5 -0
- package/docs/architecture.md +234 -0
- package/docs/compatibility.md +36 -0
- package/docs/event-manager.md +166 -0
- package/docs/platform-targets.md +108 -0
- package/docs/publishing.md +203 -0
- package/docs/reference/README.md +117 -0
- package/docs/reference/arcane-ollama.md +288 -0
- package/docs/reference/availability-and-normalization.md +123 -0
- package/docs/reference/behavioral-testing.md +86 -0
- package/docs/reference/cli.md +569 -0
- package/docs/reference/core/README.md +62 -0
- package/docs/reference/core/arcane-ai-contracts.md +873 -0
- package/docs/reference/core/arcane-api.md +601 -0
- package/docs/reference/core/arcane-entities.md +65 -0
- package/docs/reference/core/arcane-events.md +134 -0
- package/docs/reference/core/ollama-module.md +181 -0
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +1909 -0
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +1057 -0
- package/docs/reference/core/reference/arcane-api/core-and-events.md +320 -0
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +610 -0
- package/docs/reference/core/reference/arcane-api/namespaces.md +1157 -0
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +1423 -0
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +315 -0
- package/docs/reference/event-manager.md +957 -0
- package/docs/reference/inventory/package-api.json +2632 -0
- package/docs/reference/inventory/runtime-components.json +934 -0
- package/docs/reference/inventory/runtime-entities.json +26 -0
- package/docs/reference/inventory/runtime-modules.json +1249 -0
- package/docs/reference/protocols.md +242 -0
- package/docs/reference/runtime-components.md +1098 -0
- package/docs/reference/runtime-entities.md +303 -0
- package/docs/reference/runtime-modules.md +2010 -0
- package/docs/reference/sdk-api.md +4901 -0
- package/docs/roadmap.md +79 -0
- package/docs/work-amplification.md +124 -0
- package/node_modules/event-pubsub/CHANGELOG.md +55 -0
- package/node_modules/event-pubsub/MIGRATION.md +70 -0
- package/node_modules/event-pubsub/README.md +363 -0
- package/node_modules/event-pubsub/SECURITY.md +37 -0
- package/node_modules/event-pubsub/index.js +141 -0
- package/node_modules/event-pubsub/licence +21 -0
- package/node_modules/event-pubsub/package.json +59 -0
- package/node_modules/strong-type/README.md +408 -0
- package/node_modules/strong-type/assets/strong-type-header.png +0 -0
- package/node_modules/strong-type/index.js +1151 -0
- package/node_modules/strong-type/licence +21 -0
- package/node_modules/strong-type/node.js +125 -0
- package/node_modules/strong-type/package.json +61 -0
- package/package.json +95 -0
- package/runtime/ARCANE_RUNTIME_RELEASE.json +791 -0
- package/runtime/arcane/components/app-bar.html +468 -0
- package/runtime/arcane/components/assistant-panel.html +715 -0
- package/runtime/arcane/components/calculator.html +8 -0
- package/runtime/arcane/components/chart.html +655 -0
- package/runtime/arcane/components/chat.html +1225 -0
- package/runtime/arcane/components/conversation-view.html +13 -0
- package/runtime/arcane/components/dashboard-config.html +341 -0
- package/runtime/arcane/components/data-maintenance.html +112 -0
- package/runtime/arcane/components/data-view.html +92 -0
- package/runtime/arcane/components/directory-picker.html +197 -0
- package/runtime/arcane/components/document-inspector.html +252 -0
- package/runtime/arcane/components/file-drop.html +264 -0
- package/runtime/arcane/components/file-inspector.html +293 -0
- package/runtime/arcane/components/file-manager.html +1715 -0
- package/runtime/arcane/components/header.html +142 -0
- package/runtime/arcane/components/integration-settings.html +14 -0
- package/runtime/arcane/components/local-ai-status.html +360 -0
- package/runtime/arcane/components/markdown-document.html +1048 -0
- package/runtime/arcane/components/markdown-editor.html +360 -0
- package/runtime/arcane/components/media-embed.html +8 -0
- package/runtime/arcane/components/modal.html +402 -0
- package/runtime/arcane/components/output-panel.html +259 -0
- package/runtime/arcane/components/preferences-form.html +135 -0
- package/runtime/arcane/components/record-timeline.html +105 -0
- package/runtime/arcane/components/relationship-board.html +116 -0
- package/runtime/arcane/components/screen-capture.html +8 -0
- package/runtime/arcane/components/source-code-viewer.html +441 -0
- package/runtime/arcane/components/source-explanation.html +124 -0
- package/runtime/arcane/components/speech.html +365 -0
- package/runtime/arcane/components/summary-strip.html +177 -0
- package/runtime/arcane/components/table.html +77 -0
- package/runtime/arcane/components/task-progress.html +282 -0
- package/runtime/arcane/components/terminal-workspace.html +65 -0
- package/runtime/arcane/components/theme-editor.html +41 -0
- package/runtime/arcane/components/theme-switcher.html +46 -0
- package/runtime/arcane/components/unified-inbox.html +20 -0
- package/runtime/arcane/components/voice-transcription.html +476 -0
- package/runtime/arcane/components/weather-widget.html +8 -0
- package/runtime/arcane/components/web-navigator.html +239 -0
- package/runtime/arcane/css/communications.css +1 -0
- package/runtime/arcane/css/dashboard-config.css +45 -0
- package/runtime/arcane/css/document-site.css +981 -0
- package/runtime/arcane/css/layout.css +438 -0
- package/runtime/arcane/css/primitives.css +321 -0
- package/runtime/arcane/css/theme.css +112 -0
- package/runtime/arcane/css/utility-workspace.css +1 -0
- package/runtime/arcane/entities/ApiModelRecord.js +20 -0
- package/runtime/arcane/entities/Calculation.js +13 -0
- package/runtime/arcane/entities/Chat.js +581 -0
- package/runtime/arcane/entities/CommunicationMessage.js +29 -0
- package/runtime/arcane/entities/CommunicationThread.js +21 -0
- package/runtime/arcane/entities/Document.js +10 -0
- package/runtime/arcane/entities/File.js +143 -0
- package/runtime/arcane/entities/Image.js +135 -0
- package/runtime/arcane/entities/IntentEnvelope.js +834 -0
- package/runtime/arcane/entities/Preference.js +83 -0
- package/runtime/arcane/entities/TWiNPolicyDecision.js +1092 -0
- package/runtime/arcane/entities/TerminalSession.js +46 -0
- package/runtime/arcane/entities/Theme.js +107 -0
- package/runtime/arcane/entities/User.js +1046 -0
- package/runtime/arcane/entities/Weather.js +23 -0
- package/runtime/arcane/img/arcane-os-everywhere.png +0 -0
- package/runtime/arcane/img/arrow-left.png +0 -0
- package/runtime/arcane/img/arrow-right.png +0 -0
- package/runtime/arcane/img/doc.svg +5 -0
- package/runtime/arcane/img/folder.svg +4 -0
- package/runtime/arcane/img/image.svg +5 -0
- package/runtime/arcane/img/refresh.png +0 -0
- package/runtime/arcane/img/send.svg +9 -0
- package/runtime/arcane/img/trash.svg +5 -0
- package/runtime/arcane/img/upload.svg +5 -0
- package/runtime/arcane/modules/AI.js +2048 -0
- package/runtime/arcane/modules/AIPreferenceRuntime.js +32 -0
- package/runtime/arcane/modules/AIPreferenceTuple.js +92 -0
- package/runtime/arcane/modules/AIResponseLength.js +42 -0
- package/runtime/arcane/modules/AIResponseURLPolicy.js +626 -0
- package/runtime/arcane/modules/AnsiText.js +53 -0
- package/runtime/arcane/modules/ApiModelDatabase.js +25 -0
- package/runtime/arcane/modules/AppDataScope.js +245 -0
- package/runtime/arcane/modules/AppearancePreferences.js +28 -0
- package/runtime/arcane/modules/ArcaneCommunicationBridge.js +22 -0
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +135 -0
- package/runtime/arcane/modules/ArcaneNetworkPolicy.js +255 -0
- package/runtime/arcane/modules/AsyncBoundary.js +161 -0
- package/runtime/arcane/modules/BrowserTestSuite.js +326 -0
- package/runtime/arcane/modules/CalculatorEngine.js +20 -0
- package/runtime/arcane/modules/CaseEvidenceIndexer.js +134 -0
- package/runtime/arcane/modules/ChartLibrary.js +35 -0
- package/runtime/arcane/modules/ChatRecords.js +13 -0
- package/runtime/arcane/modules/CommunicationAppController.js +43 -0
- package/runtime/arcane/modules/CommunicationHub.js +16 -0
- package/runtime/arcane/modules/CommunicationPreferences.js +13 -0
- package/runtime/arcane/modules/CommunicationProviderRegistry.js +16 -0
- package/runtime/arcane/modules/ComponentContracts.js +586 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +288 -0
- package/runtime/arcane/modules/ConversationActionItems.js +488 -0
- package/runtime/arcane/modules/ConversationClosingReport.js +274 -0
- package/runtime/arcane/modules/ConversationTimebox.js +527 -0
- package/runtime/arcane/modules/CoreLocalModelCatalog.js +255 -0
- package/runtime/arcane/modules/DBLS.js +171 -0
- package/runtime/arcane/modules/DBOPFS.js +1154 -0
- package/runtime/arcane/modules/DBOPFSWorker.js +116 -0
- package/runtime/arcane/modules/DataMaintenance.js +91 -0
- package/runtime/arcane/modules/DevelopmentWorkspace.js +74 -0
- package/runtime/arcane/modules/DirectoryPicker.js +109 -0
- package/runtime/arcane/modules/DocumentNavigation.js +223 -0
- package/runtime/arcane/modules/Errors.js +1025 -0
- package/runtime/arcane/modules/GifEncoder.js +29 -0
- package/runtime/arcane/modules/HTMLImport.js +114 -0
- package/runtime/arcane/modules/InMemoryCommunicationProvider.js +14 -0
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +275 -0
- package/runtime/arcane/modules/LocalAIReadiness.js +870 -0
- package/runtime/arcane/modules/LocalAIReadinessController.js +156 -0
- package/runtime/arcane/modules/MD.js +111 -0
- package/runtime/arcane/modules/Mail.js +352 -0
- package/runtime/arcane/modules/MailTransport.mjs +180 -0
- package/runtime/arcane/modules/Marked.min.js +71 -0
- package/runtime/arcane/modules/MemoryRecords.js +44 -0
- package/runtime/arcane/modules/MessageAdvisory.js +38 -0
- package/runtime/arcane/modules/ModelDefinition.js +189 -0
- package/runtime/arcane/modules/Ollama.js +74 -0
- package/runtime/arcane/modules/OllamaModelIdentifier.js +21 -0
- package/runtime/arcane/modules/OllamaSettings.js +24 -0
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +14 -0
- package/runtime/arcane/modules/PreferenceStore.js +109 -0
- package/runtime/arcane/modules/QRCode.min.js +1 -0
- package/runtime/arcane/modules/Questionnaire.js +61 -0
- package/runtime/arcane/modules/RecordLinkIndex.js +24 -0
- package/runtime/arcane/modules/RecordPassageIndex.js +222 -0
- package/runtime/arcane/modules/RecordReviewStore.js +94 -0
- package/runtime/arcane/modules/RevocableProjectionLedger.js +1623 -0
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +37 -0
- package/runtime/arcane/modules/ScamRiskPolicy.js +62 -0
- package/runtime/arcane/modules/ScopedOPFSCache.js +183 -0
- package/runtime/arcane/modules/ScreenCapture.js +20 -0
- package/runtime/arcane/modules/SpeechPlayback.js +581 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +1248 -0
- package/runtime/arcane/modules/SystemAppearance.js +20 -0
- package/runtime/arcane/modules/SystemPlatformPresentation.js +51 -0
- package/runtime/arcane/modules/SystemToolRegistry.js +28 -0
- package/runtime/arcane/modules/TerminalClient.js +52 -0
- package/runtime/arcane/modules/TerminalCommandRegistry.js +50 -0
- package/runtime/arcane/modules/ThemeBootstrap.js +26 -0
- package/runtime/arcane/modules/ThemeManager.js +131 -0
- package/runtime/arcane/modules/TimeGuard.js +149 -0
- package/runtime/arcane/modules/ToolCallRouter.js +83 -0
- package/runtime/arcane/modules/WaitForComponent.js +102 -0
- package/runtime/arcane/modules/YouTubeMedia.js +16 -0
- package/runtime/arcane/modules/uPlot.LICENSE.txt +21 -0
- package/runtime/arcane/modules/uPlot.iife.min.js +2 -0
- package/runtime/arcane/modules/uPlot.min.css +1 -0
- package/runtime/arcane/security/arcane-network-policy.json +6 -0
- package/runtime/strong-type/index.js +352 -0
- package/runtime/strong-type/licence +21 -0
- package/runtime/strong-type/package.json +45 -0
- package/schemas/arcane-app-bundle.schema.json +125 -0
- package/schemas/arcane-app.schema.json +329 -0
- package/schemas/arcane-lock.schema.json +86 -0
- package/schemas/arcane-package.schema.json +224 -0
- package/schemas/cli-event.schema.json +122 -0
- package/schemas/event-stack.schema.json +152 -0
- package/schemas/native-build-plan.schema.json +176 -0
- package/schemas/target-adapter.schema.json +117 -0
- package/src/app-descriptor.mjs +500 -0
- package/src/cli/main.mjs +561 -0
- package/src/constants.mjs +31 -0
- package/src/dev-server.mjs +718 -0
- package/src/doctor.mjs +315 -0
- package/src/dom-event-instrumentation.mjs +594 -0
- package/src/errors.mjs +75 -0
- package/src/event-manager.mjs +1342 -0
- package/src/event-queue.mjs +138 -0
- package/src/events.mjs +219 -0
- package/src/index.mjs +177 -0
- package/src/integrated-provider-loader.mjs +432 -0
- package/src/native-plan.mjs +698 -0
- package/src/native-provider-loader.mjs +1126 -0
- package/src/packager/core.mjs +2691 -0
- package/src/process.mjs +353 -0
- package/src/release-bundle.mjs +2523 -0
- package/src/repository.mjs +90 -0
- package/src/runtime.mjs +452 -0
- package/src/scaffold.mjs +380 -0
- package/src/targets/index.mjs +436 -0
- package/src/templates/assets/app-icon.png +0 -0
- package/src/templates/workspace-template.mjs +388 -0
- package/src/testing-loader.mjs +9 -0
- package/src/testing.mjs +427 -0
- package/src/toolchain.mjs +1335 -0
- package/src/update-check.mjs +307 -0
- package/src/workspace.mjs +449 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkLocalAIReadiness,
|
|
3
|
+
deriveLocalAIRequirements
|
|
4
|
+
} from './LocalAIReadiness.js?v=4';
|
|
5
|
+
|
|
6
|
+
const SLOT_NAMES=Object.freeze(['llm','stt','tts']);
|
|
7
|
+
|
|
8
|
+
function availabilityFromReport(report={}){
|
|
9
|
+
const slots=report.slots||{};
|
|
10
|
+
return Object.freeze(Object.fromEntries(SLOT_NAMES.map(name=>{
|
|
11
|
+
const slot=slots[name]||{};
|
|
12
|
+
return [name,!slot.required||slot.ready===true];
|
|
13
|
+
})));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function selectedPreferences(source){
|
|
17
|
+
return typeof source==='function'?source():source;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pendingReport(requirements){
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
slots:Object.freeze(Object.fromEntries(SLOT_NAMES.map(name=>[
|
|
23
|
+
name,
|
|
24
|
+
Object.freeze({
|
|
25
|
+
...requirements[name],
|
|
26
|
+
ready:requirements[name].required?false:null
|
|
27
|
+
})
|
|
28
|
+
])))
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function waitForStatusComponent(status){
|
|
33
|
+
if(!status||status.ready){
|
|
34
|
+
return status;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await new Promise(resolve=>{
|
|
38
|
+
const timeout=globalThis.setTimeout(done,10000);
|
|
39
|
+
function done(){
|
|
40
|
+
globalThis.clearTimeout(timeout);
|
|
41
|
+
status.removeEventListener('local-ai-status-ready',done);
|
|
42
|
+
resolve();
|
|
43
|
+
}
|
|
44
|
+
status.addEventListener('local-ai-status-ready',done,{once:true});
|
|
45
|
+
});
|
|
46
|
+
return status;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Connects the shared readiness contract to a chat surface. Applications own
|
|
51
|
+
* only their preference source and navigation destinations.
|
|
52
|
+
*/
|
|
53
|
+
export function createLocalAIReadinessController({
|
|
54
|
+
chat,
|
|
55
|
+
status,
|
|
56
|
+
preferences,
|
|
57
|
+
profileHref=null,
|
|
58
|
+
onChange,
|
|
59
|
+
...checkOptions
|
|
60
|
+
}={}){
|
|
61
|
+
if(!chat){
|
|
62
|
+
throw new TypeError('A chat component is required.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let active=null;
|
|
66
|
+
let latestReport=null;
|
|
67
|
+
let currentAvailability=availabilityFromReport({});
|
|
68
|
+
|
|
69
|
+
const publishAvailability=()=>{
|
|
70
|
+
if(typeof chat.setAIAvailability==='function'){
|
|
71
|
+
chat.setAIAvailability(currentAvailability);
|
|
72
|
+
}else{
|
|
73
|
+
chat.addEventListener('chat-ready',publishAvailability,{once:true});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const applyAvailability=report=>{
|
|
78
|
+
currentAvailability=availabilityFromReport(report);
|
|
79
|
+
publishAvailability();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const check=async()=>{
|
|
83
|
+
if(active){
|
|
84
|
+
return active;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
active=(async()=>{
|
|
88
|
+
const preferenceTuple=selectedPreferences(preferences);
|
|
89
|
+
const requirements=deriveLocalAIRequirements(preferenceTuple);
|
|
90
|
+
const required=SLOT_NAMES.some(name=>requirements[name].required);
|
|
91
|
+
|
|
92
|
+
applyAvailability(pendingReport(requirements));
|
|
93
|
+
await waitForStatusComponent(status);
|
|
94
|
+
status?.configure?.({profileHref});
|
|
95
|
+
if(required){
|
|
96
|
+
status?.begin?.(requirements);
|
|
97
|
+
}else if(status){
|
|
98
|
+
status.hidden=true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
latestReport=await checkLocalAIReadiness({
|
|
102
|
+
...checkOptions,
|
|
103
|
+
preferences:preferenceTuple
|
|
104
|
+
});
|
|
105
|
+
status?.present?.(latestReport);
|
|
106
|
+
applyAvailability(latestReport);
|
|
107
|
+
if(typeof onChange==='function'){
|
|
108
|
+
onChange(latestReport);
|
|
109
|
+
}
|
|
110
|
+
chat.dispatchEvent(new CustomEvent('local-ai-readiness-change',{
|
|
111
|
+
bubbles:true,
|
|
112
|
+
composed:true,
|
|
113
|
+
detail:{report:latestReport}
|
|
114
|
+
}));
|
|
115
|
+
return latestReport;
|
|
116
|
+
})().finally(()=>{
|
|
117
|
+
active=null;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return active;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ensure=()=>{
|
|
124
|
+
if(active){
|
|
125
|
+
return active;
|
|
126
|
+
}
|
|
127
|
+
return latestReport
|
|
128
|
+
?Promise.resolve(latestReport)
|
|
129
|
+
:check();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const retry=()=>void check();
|
|
133
|
+
status?.addEventListener('local-ai-retry',retry);
|
|
134
|
+
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
check,
|
|
137
|
+
ensure,
|
|
138
|
+
destroy(){
|
|
139
|
+
status?.removeEventListener('local-ai-retry',retry);
|
|
140
|
+
},
|
|
141
|
+
get availability(){
|
|
142
|
+
return currentAvailability;
|
|
143
|
+
},
|
|
144
|
+
get report(){
|
|
145
|
+
return latestReport;
|
|
146
|
+
},
|
|
147
|
+
readyFor(name){
|
|
148
|
+
if(!SLOT_NAMES.includes(name)){
|
|
149
|
+
throw new TypeError('Local AI slot must be llm, stt, or tts.');
|
|
150
|
+
}
|
|
151
|
+
return currentAvailability[name];
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export {availabilityFromReport};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { marked } from './Marked.min.js';
|
|
2
|
+
import Is from '../../node_modules/strong-type/index.js';
|
|
3
|
+
|
|
4
|
+
marked.use(
|
|
5
|
+
{
|
|
6
|
+
async: false,
|
|
7
|
+
pedantic: false,
|
|
8
|
+
gfm: true,
|
|
9
|
+
renderer: {
|
|
10
|
+
link(href, title, text) {
|
|
11
|
+
const link = marked.Renderer.prototype.link.call(this, href, title, text);
|
|
12
|
+
return link.replace("<a","<a target='_blank' ");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const is = new Is(false);
|
|
19
|
+
|
|
20
|
+
class MD {
|
|
21
|
+
#raw='';
|
|
22
|
+
#rendered=''
|
|
23
|
+
|
|
24
|
+
constructor(raw=''){
|
|
25
|
+
this.raw=raw;
|
|
26
|
+
this.rendered=marked.parse(raw);
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get rendered(){
|
|
31
|
+
return this.#rendered;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get safeRendered(){
|
|
35
|
+
return sanitize(this.#rendered);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
set rendered(value=''){
|
|
39
|
+
return this.#rendered;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get raw(){
|
|
43
|
+
return this.#raw;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
set raw(value=''){
|
|
47
|
+
if(!is.string(value)){
|
|
48
|
+
console.trace('MD.raw must be a string.');
|
|
49
|
+
return this.#raw;
|
|
50
|
+
}
|
|
51
|
+
this.#raw = value;
|
|
52
|
+
this.#rendered = marked.parse(value);
|
|
53
|
+
return this.#raw;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
append(value=''){
|
|
57
|
+
if(!is.string(value)){
|
|
58
|
+
console.trace('MD.append must be a string.');
|
|
59
|
+
return this.#raw;
|
|
60
|
+
}
|
|
61
|
+
this.#raw += value;
|
|
62
|
+
this.#rendered = marked.parse(this.#raw);
|
|
63
|
+
return this.#raw;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sanitize(html=''){
|
|
68
|
+
const template=document.createElement('template');
|
|
69
|
+
template.innerHTML=html;
|
|
70
|
+
|
|
71
|
+
template.content.querySelectorAll(
|
|
72
|
+
'script,style,iframe,object,embed,link,meta,base,form,input,button,textarea,select,option,svg,math'
|
|
73
|
+
).forEach(element=>element.remove());
|
|
74
|
+
|
|
75
|
+
template.content.querySelectorAll('*').forEach(
|
|
76
|
+
function sanitizeElement(element){
|
|
77
|
+
const attributes=Array.from(element.attributes);
|
|
78
|
+
|
|
79
|
+
for(let i=0;i<attributes.length;i++){
|
|
80
|
+
const attribute=attributes[i];
|
|
81
|
+
const name=attribute.name.toLowerCase();
|
|
82
|
+
|
|
83
|
+
if(name.startsWith('on')||name==='style'||name==='srcdoc'){
|
|
84
|
+
element.removeAttribute(attribute.name);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if(!['href','src','xlink:href'].includes(name)){
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const value=attribute.value
|
|
93
|
+
.replace(/[\u0000-\u001F\u007F\s]+/g,'')
|
|
94
|
+
.toLowerCase();
|
|
95
|
+
const safeImage=name==='src'&&value.startsWith('data:image/');
|
|
96
|
+
|
|
97
|
+
if(
|
|
98
|
+
value.startsWith('javascript:')
|
|
99
|
+
|| value.startsWith('vbscript:')
|
|
100
|
+
|| value.startsWith('data:')&&!safeImage
|
|
101
|
+
){
|
|
102
|
+
element.removeAttribute(attribute.name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return template.innerHTML;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default MD;
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_MAIL_REQUEST_TIMEOUT_MS,
|
|
3
|
+
sendMailReport,
|
|
4
|
+
} from './MailTransport.mjs';
|
|
5
|
+
|
|
6
|
+
let userInstance=null;
|
|
7
|
+
|
|
8
|
+
async function loadOptionalMailDependencies(){
|
|
9
|
+
await import('./DBOPFS.js');
|
|
10
|
+
if(!userInstance){
|
|
11
|
+
const { default:UserEntity }=await import('../entities/User.js');
|
|
12
|
+
userInstance=new UserEntity();
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
dbopfs:globalThis.dbopfs,
|
|
16
|
+
user:userInstance,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MAX_SUBJECT_LENGTH=160;
|
|
21
|
+
const MAX_MESSAGE_BYTES=25*1024*1024;
|
|
22
|
+
const MAX_NATIVE_REPORT_BYTES=768*1024;
|
|
23
|
+
const MAIL_TYPES=new Set(['error','report','crisis_detected']);
|
|
24
|
+
const EMAIL_PATTERN=/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
|
25
|
+
const ARCANE_APP_ID_PATTERN=/^[a-z0-9](?:[a-z0-9-]{0,62})$/;
|
|
26
|
+
|
|
27
|
+
function declaredApplicationId(document=globalThis.document){
|
|
28
|
+
const value=document?.querySelector?.('meta[name="arcane-app-id"]')?.content?.trim();
|
|
29
|
+
return ARCANE_APP_ID_PATTERN.test(value||'') ? value:'';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function declaredMailBaseDomain(document=globalThis.document){
|
|
33
|
+
return normalizeBaseDomain(
|
|
34
|
+
document?.querySelector?.('meta[name="arcane-mail-base-domain"]')?.content,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeBaseDomain(value){
|
|
39
|
+
const domain=String(value||'').trim().toLowerCase();
|
|
40
|
+
if(!domain||domain.length>253
|
|
41
|
+
|| !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(domain)){
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
return domain;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hostedBaseDomain(hostname,configuredBaseDomain=''){
|
|
48
|
+
const configured=normalizeBaseDomain(configuredBaseDomain);
|
|
49
|
+
return configured&&(hostname===configured||hostname.endsWith(`.${configured}`)) ? configured:'';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function defaultMailEndpoint(location=globalThis.location,baseDomain=''){
|
|
53
|
+
if(!location||!['http:','https:'].includes(location.protocol)){
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
const hostname=String(location.hostname||'').toLowerCase();
|
|
57
|
+
const loopback=['localhost','127.0.0.1','::1','[::1]'].includes(hostname);
|
|
58
|
+
if(loopback&&location.protocol==='http:'&&String(location.port||'')!=='8025'){
|
|
59
|
+
const authority=hostname==='::1' ? '[::1]':hostname;
|
|
60
|
+
return `http://${authority}:8025/v1/mail`;
|
|
61
|
+
}
|
|
62
|
+
if(loopback){
|
|
63
|
+
return new URL('/v1/mail',location.origin).href;
|
|
64
|
+
}
|
|
65
|
+
const root=hostedBaseDomain(hostname,baseDomain);
|
|
66
|
+
if(!root){
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
return `https://mail.${root}/v1/mail`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolveMailConfig(
|
|
73
|
+
config=globalThis.arcane?.config?.mail||{},
|
|
74
|
+
{document=globalThis.document,location=globalThis.location}={}
|
|
75
|
+
){
|
|
76
|
+
const supplied=config&&typeof config==='object'&&!Array.isArray(config)?config:{};
|
|
77
|
+
const appName=typeof supplied.appName==='string'&&supplied.appName.trim()
|
|
78
|
+
? supplied.appName.trim()
|
|
79
|
+
: declaredApplicationId(document);
|
|
80
|
+
return Object.freeze({
|
|
81
|
+
appName:ARCANE_APP_ID_PATTERN.test(appName) ? appName:'',
|
|
82
|
+
appKey:typeof supplied.appKey==='string' ? supplied.appKey:'',
|
|
83
|
+
endpoint:typeof supplied.endpoint==='string'&&supplied.endpoint.trim()
|
|
84
|
+
? supplied.endpoint.trim()
|
|
85
|
+
: defaultMailEndpoint(location,supplied.baseDomain||declaredMailBaseDomain(document)),
|
|
86
|
+
requestTimeout:Number.isFinite(supplied.requestTimeout)
|
|
87
|
+
? supplied.requestTimeout
|
|
88
|
+
: DEFAULT_MAIL_REQUEST_TIMEOUT_MS,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function escapeHtml(value){
|
|
93
|
+
return String(value)
|
|
94
|
+
.replaceAll('&','&')
|
|
95
|
+
.replaceAll('<','<')
|
|
96
|
+
.replaceAll('>','>')
|
|
97
|
+
.replaceAll('"','"')
|
|
98
|
+
.replaceAll("'",''');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function utf8ByteLength(value){
|
|
102
|
+
return new TextEncoder().encode(value).byteLength;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function clonePayload(value){
|
|
106
|
+
if(typeof globalThis.structuredClone==='function'){
|
|
107
|
+
return globalThis.structuredClone(value);
|
|
108
|
+
}
|
|
109
|
+
return JSON.parse(JSON.stringify(value));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function serializePayload(value){
|
|
113
|
+
try{
|
|
114
|
+
return JSON.stringify(value,null,2);
|
|
115
|
+
}catch{
|
|
116
|
+
return '[The structured payload could not be serialized.]';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function assertMessageSize(report){
|
|
121
|
+
const messageBytes=['text','html'].reduce(
|
|
122
|
+
(total,key)=>total+utf8ByteLength(report[key]||''),
|
|
123
|
+
0
|
|
124
|
+
);
|
|
125
|
+
if(messageBytes>MAX_MESSAGE_BYTES){
|
|
126
|
+
throw new Error('Generated report email content exceeds the 25 MiB limit');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function assertNativeReportSize(report){
|
|
131
|
+
const serialized=JSON.stringify(report);
|
|
132
|
+
if(utf8ByteLength(serialized)>MAX_NATIVE_REPORT_BYTES){
|
|
133
|
+
throw new RangeError('Native Arcane mail reports cannot exceed 786,432 serialized bytes');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function createReportKey(subject){
|
|
138
|
+
const safeSubject=subject
|
|
139
|
+
.normalize('NFKD')
|
|
140
|
+
.replace(/[^a-z0-9_-]+/gi,'_')
|
|
141
|
+
.replace(/^_+|_+$/g,'')
|
|
142
|
+
.slice(0,48)||'notification';
|
|
143
|
+
let nonce;
|
|
144
|
+
if(typeof globalThis.crypto?.randomUUID==='function'){
|
|
145
|
+
nonce=globalThis.crypto.randomUUID();
|
|
146
|
+
}else if(typeof globalThis.crypto?.getRandomValues==='function'){
|
|
147
|
+
const bytes=globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
148
|
+
nonce=[...bytes].map(value=>value.toString(16).padStart(2,'0')).join('');
|
|
149
|
+
}else{
|
|
150
|
+
nonce=Math.random().toString(36).slice(2).padEnd(16,'0').slice(0,16);
|
|
151
|
+
}
|
|
152
|
+
return `${Date.now()}-${nonce}-email-${safeSubject}.json`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeRecipients(values){
|
|
156
|
+
if(!Array.isArray(values)){
|
|
157
|
+
throw new TypeError('Mail recipients must be an array');
|
|
158
|
+
}
|
|
159
|
+
if(values.length>50){
|
|
160
|
+
throw new TypeError('Mail supports no more than 50 recipients');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const recipients=[];
|
|
164
|
+
for(const value of values){
|
|
165
|
+
if(!value){
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if(typeof value!=='string'){
|
|
170
|
+
throw new TypeError('Every mail recipient must be an email address');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const address=value.trim().toLowerCase();
|
|
174
|
+
if(address.length>254||!EMAIL_PATTERN.test(address)){
|
|
175
|
+
throw new TypeError('Mail contains an invalid recipient address');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if(!recipients.includes(address)){
|
|
179
|
+
recipients.push(address);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return recipients;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class Mail {
|
|
187
|
+
constructor(config=globalThis.arcane?.config?.mail||{}) {
|
|
188
|
+
if(globalThis.window?.mail){
|
|
189
|
+
return globalThis.window.mail;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const resolved=resolveMailConfig(config);
|
|
193
|
+
this.appName=resolved.appName;
|
|
194
|
+
this.appKey=resolved.appKey;
|
|
195
|
+
this.endpoint=resolved.endpoint;
|
|
196
|
+
this.requestTimeout=resolved.requestTimeout;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#assertConfigured(){
|
|
200
|
+
if(!this.appName||(!this.endpoint&&typeof globalThis.Arcane?.mail?.send!=='function')){
|
|
201
|
+
throw new Error('Mail transport is not configured');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async #deliver(report,reportKey){
|
|
206
|
+
if(typeof globalThis.Arcane?.mail?.send==='function'){
|
|
207
|
+
assertNativeReportSize(report);
|
|
208
|
+
return globalThis.Arcane.mail.send({report,reportKey});
|
|
209
|
+
}
|
|
210
|
+
return sendMailReport({
|
|
211
|
+
appKey:this.appKey,
|
|
212
|
+
appName:this.appName,
|
|
213
|
+
endpoint:this.endpoint,
|
|
214
|
+
report,
|
|
215
|
+
reportKey,
|
|
216
|
+
requestTimeout:this.requestTimeout,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async send(to=[], subject='', payload={}, messageStyle='', messageType='') {
|
|
221
|
+
const normalizedSubject=typeof subject==='string' ? subject.trim():'';
|
|
222
|
+
if(!normalizedSubject||normalizedSubject.length>MAX_SUBJECT_LENGTH||/[\u0000-\u001f\u007f]/.test(normalizedSubject)){
|
|
223
|
+
throw new TypeError(`Mail subject must contain 1-${MAX_SUBJECT_LENGTH} characters without line breaks`);
|
|
224
|
+
}
|
|
225
|
+
if(!payload||typeof payload!=='object'||Array.isArray(payload)){
|
|
226
|
+
throw new TypeError('Mail payload must be an object');
|
|
227
|
+
}
|
|
228
|
+
if(typeof messageStyle!=='string'){
|
|
229
|
+
throw new TypeError('Mail message style must be a string');
|
|
230
|
+
}
|
|
231
|
+
if(!Array.isArray(to)){
|
|
232
|
+
throw new TypeError('Mail recipients must be an array');
|
|
233
|
+
}
|
|
234
|
+
if(typeof messageType!=='string'||!MAIL_TYPES.has(messageType)){
|
|
235
|
+
throw new TypeError('Mail type must be error, report, or crisis_detected');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.#assertConfigured();
|
|
239
|
+
|
|
240
|
+
const recipients=normalizeRecipients(to);
|
|
241
|
+
if(messageType!=='error'&&recipients.length===0){
|
|
242
|
+
throw new TypeError('Report and crisis mail require at least one recipient');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const reportKey=createReportKey(normalizedSubject);
|
|
246
|
+
const reportPayload={
|
|
247
|
+
...clonePayload(payload),
|
|
248
|
+
source_at:new Date().toISOString(),
|
|
249
|
+
source_path:globalThis.location?.pathname||'',
|
|
250
|
+
subject:normalizedSubject,
|
|
251
|
+
type:messageType,
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if(messageType==='error'){
|
|
255
|
+
reportPayload.report={
|
|
256
|
+
subject:normalizedSubject,
|
|
257
|
+
text:`${this.appName} application error\n\n${serializePayload(reportPayload)}`,
|
|
258
|
+
to:recipients,
|
|
259
|
+
type:messageType,
|
|
260
|
+
};
|
|
261
|
+
assertMessageSize(reportPayload.report);
|
|
262
|
+
|
|
263
|
+
const delivery=await this.#deliver(reportPayload.report,reportKey);
|
|
264
|
+
return { ...delivery,reportKey };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let reportStorage=null;
|
|
268
|
+
let profile={ email:'',language:'',phone:'',username:'' };
|
|
269
|
+
try{
|
|
270
|
+
const dependencies=await loadOptionalMailDependencies();
|
|
271
|
+
reportStorage=dependencies.dbopfs;
|
|
272
|
+
try{
|
|
273
|
+
await dependencies.user.load();
|
|
274
|
+
}catch(error){
|
|
275
|
+
console.warn('Unable to load the user profile for mail; continuing with available values.',error);
|
|
276
|
+
}
|
|
277
|
+
profile={
|
|
278
|
+
email:dependencies.user.email,
|
|
279
|
+
language:dependencies.user.language,
|
|
280
|
+
phone:dependencies.user.phone,
|
|
281
|
+
username:dependencies.user.username,
|
|
282
|
+
};
|
|
283
|
+
}catch(error){
|
|
284
|
+
console.warn('Optional mail formatting dependencies are unavailable; using a deterministic fallback.',error);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const wantsHtml=/\bhtml\b/i.test(messageStyle);
|
|
288
|
+
Object.assign(reportPayload,{
|
|
289
|
+
email:profile.email,
|
|
290
|
+
language:profile.language,
|
|
291
|
+
phone:profile.phone,
|
|
292
|
+
source_user:profile.username || 'username not specified',
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Email formatting stays local and deterministic. The supplied style
|
|
296
|
+
// is presentation intent only; it is never sent to an AI provider and
|
|
297
|
+
// untrusted report values are escaped before entering HTML.
|
|
298
|
+
const serialized=serializePayload(reportPayload);
|
|
299
|
+
const generatedMessage=wantsHtml
|
|
300
|
+
? `<pre>${escapeHtml(serialized)}</pre>`
|
|
301
|
+
: serialized;
|
|
302
|
+
|
|
303
|
+
const sourceUser=String(reportPayload.source_user)
|
|
304
|
+
.replace(/[\u0000-\u001f\u007f]/g,' ')
|
|
305
|
+
.trim()
|
|
306
|
+
.slice(0,80);
|
|
307
|
+
const subjectSuffix=sourceUser ? ` - ${sourceUser}`:'';
|
|
308
|
+
const deliverySubject=normalizedSubject.length+subjectSuffix.length<=MAX_SUBJECT_LENGTH
|
|
309
|
+
? `${normalizedSubject}${subjectSuffix}`
|
|
310
|
+
: normalizedSubject;
|
|
311
|
+
|
|
312
|
+
reportPayload.report = {
|
|
313
|
+
subject:deliverySubject,
|
|
314
|
+
to:recipients,
|
|
315
|
+
type:messageType,
|
|
316
|
+
};
|
|
317
|
+
if(wantsHtml){
|
|
318
|
+
reportPayload.report.html=`${generatedMessage}
|
|
319
|
+
<hr>
|
|
320
|
+
<p>Phone: ${escapeHtml(profile.phone || 'not provided')}<br>Email: ${escapeHtml(profile.email || 'not provided')}</p>`;
|
|
321
|
+
}else{
|
|
322
|
+
reportPayload.report.text=`${generatedMessage}
|
|
323
|
+
|
|
324
|
+
Phone: ${profile.phone || 'not provided'}
|
|
325
|
+
Email: ${profile.email || 'not provided'}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
assertMessageSize(reportPayload.report);
|
|
329
|
+
|
|
330
|
+
try{
|
|
331
|
+
if(!reportStorage?.set){
|
|
332
|
+
throw new Error('Report storage is unavailable');
|
|
333
|
+
}
|
|
334
|
+
await reportStorage.set('reports',reportKey,reportPayload);
|
|
335
|
+
}catch(error){
|
|
336
|
+
console.warn('Unable to store the generated mail report; continuing with delivery.',error);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const delivery=await this.#deliver(reportPayload.report,reportKey);
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
...delivery,
|
|
343
|
+
reportKey,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if(globalThis.window&&!globalThis.window.mail){
|
|
349
|
+
globalThis.window.mail=new Mail();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export default Mail;
|