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,37 @@
|
|
|
1
|
+
const DEFAULT_LEVELS=Object.freeze([
|
|
2
|
+
{minimum:70,id:'critical'},
|
|
3
|
+
{minimum:40,id:'high'},
|
|
4
|
+
{minimum:15,id:'caution'},
|
|
5
|
+
{minimum:0,id:'low'},
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
function normalizeText(value,maxLength){
|
|
9
|
+
return String(value??'').normalize('NFKC').slice(0,maxLength);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function analyzeRiskSignals(input,{signals=[],levels=DEFAULT_LEVELS,maxLength=20_000}={}){
|
|
13
|
+
const text=normalizeText(input,maxLength);
|
|
14
|
+
const matches=[];
|
|
15
|
+
let score=0;
|
|
16
|
+
|
|
17
|
+
for(const signal of signals){
|
|
18
|
+
if(!signal||typeof signal.id!=='string'||!(signal.pattern instanceof RegExp))continue;
|
|
19
|
+
signal.pattern.lastIndex=0;
|
|
20
|
+
if(!signal.pattern.test(text))continue;
|
|
21
|
+
const weight=Math.max(0,Math.min(100,Number(signal.weight)||0));
|
|
22
|
+
score+=weight;
|
|
23
|
+
matches.push({
|
|
24
|
+
id:signal.id,
|
|
25
|
+
label:String(signal.label||signal.id),
|
|
26
|
+
weight,
|
|
27
|
+
guidance:String(signal.guidance||''),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
score=Math.min(100,score);
|
|
32
|
+
const ordered=[...levels].sort((a,b)=>b.minimum-a.minimum);
|
|
33
|
+
const level=ordered.find(candidate=>score>=candidate.minimum)?.id||'unknown';
|
|
34
|
+
return Object.freeze({level,matches:Object.freeze(matches),score,textLength:text.length,truncated:String(input??'').length>text.length});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {DEFAULT_LEVELS};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {analyzeRiskSignals} from './RiskSignalAnalyzer.js';
|
|
2
|
+
import {canonicalNetworkHostname,emptyArcaneNetworkPolicy,findDeniedDomainRule,loadArcaneNetworkPolicy} from './ArcaneNetworkPolicy.js?v=3';
|
|
3
|
+
|
|
4
|
+
export const scamRiskSignals=Object.freeze([
|
|
5
|
+
{id:'urgency',label:'Urgency or secrecy pressure',weight:18,pattern:/\b(urgent|immediately|act now|do not tell|keep (?:this|it) secret|stay on the line)\b/i,guidance:'Pause. A legitimate organization will let you verify independently.'},
|
|
6
|
+
{id:'payment',label:'Unusual payment request',weight:32,pattern:/\b(gift cards?|bitcoin|crypto(?:currency)?|wire transfer|cash courier|payment app|prepaid cards?)\b/i,guidance:'Do not pay. Contact the organization using a trusted number.'},
|
|
7
|
+
{id:'credential',label:'Credential or access request',weight:35,pattern:/\b(password|passcode|verification code|one[- ]time code|otp|remote access|screen share)\b/i,guidance:'Never share security codes or grant remote access to an unexpected contact.'},
|
|
8
|
+
{id:'impersonation',label:'Authority or family impersonation',weight:24,pattern:/\b(irs|social security|medicare|police|sheriff|grandson|granddaughter|family emergency|tech support)\b/i,guidance:'End the contact and verify through an independently found official channel.'},
|
|
9
|
+
{id:'threat',label:'Threat or fear tactic',weight:28,pattern:/\b(arrest|warrant|deport|account (?:will be )?closed|service (?:will be )?cut off|in danger|kidnapped)\b/i,guidance:'Threats are designed to prevent careful checking. Stop and contact someone you trust.'},
|
|
10
|
+
{id:'prize',label:'Unexpected prize or refund',weight:20,pattern:/\b(lottery|sweepstakes|prize|inheritance|refund|you(?: have|'ve) won)\b/i,guidance:'Do not pay a fee or disclose information to receive an unexpected benefit.'},
|
|
11
|
+
{id:'link',label:'Pressure to open a link',weight:16,pattern:/\b(click|tap|open|visit|follow)\b.{0,36}\b(link|url|website)\b/i,guidance:'Do not use an unexpected link. Open the official app or type a trusted address yourself.'},
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const blockedDomainSignal=Object.freeze({
|
|
15
|
+
id:'blocked-domain',
|
|
16
|
+
label:'Domain blocked by Arcane network policy',
|
|
17
|
+
weight:55,
|
|
18
|
+
guidance:'Do not open or contact this domain. Arcane has a system-wide safety rule blocking it.',
|
|
19
|
+
});
|
|
20
|
+
let activeNetworkPolicy=emptyArcaneNetworkPolicy();
|
|
21
|
+
let activeNetworkPolicyLoadSequence=0;
|
|
22
|
+
let nextNetworkPolicyLoadSequence=0;
|
|
23
|
+
|
|
24
|
+
function candidateHostnames(value){
|
|
25
|
+
const text=String(value??'').normalize('NFKC').slice(0,20_000),hostnames=new Set();
|
|
26
|
+
const add=value=>{
|
|
27
|
+
const candidate=String(value).replace(/^[([{<'"]+|[\])}>'",.;!?]+$/g,'');
|
|
28
|
+
try{
|
|
29
|
+
const hostname=/^https?:\/\//i.test(candidate)?new URL(candidate).hostname:candidate,canonical=canonicalNetworkHostname(hostname);
|
|
30
|
+
if(canonical)hostnames.add(canonical);
|
|
31
|
+
}catch{}
|
|
32
|
+
};
|
|
33
|
+
for(const match of text.matchAll(/\bhttps?:\/\/[^\s<>"']+/gi))add(match[0]);
|
|
34
|
+
for(const match of text.matchAll(/(?<![\p{L}\p{N}-])(?:www\.)?(?:[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?\.)+[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?(?![\p{L}\p{N}-])/giu))add(match[0]);
|
|
35
|
+
return hostnames;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function levelForScore(score){if(score>=70)return 'critical';if(score>=40)return 'high';if(score>=15)return 'caution';return 'low';}
|
|
39
|
+
|
|
40
|
+
export async function loadScamNetworkPolicy(options){
|
|
41
|
+
const loadSequence=++nextNetworkPolicyLoadSequence,networkPolicy=await loadArcaneNetworkPolicy(options);
|
|
42
|
+
if(networkPolicy.generation>activeNetworkPolicy.generation||(networkPolicy.generation===activeNetworkPolicy.generation&&loadSequence>activeNetworkPolicyLoadSequence)){
|
|
43
|
+
activeNetworkPolicy=networkPolicy;
|
|
44
|
+
activeNetworkPolicyLoadSequence=loadSequence;
|
|
45
|
+
}
|
|
46
|
+
return networkPolicy;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function assessScamRisk(text,{networkPolicy=activeNetworkPolicy}={}){
|
|
50
|
+
const result=analyzeRiskSignals(text,{signals:scamRiskSignals});
|
|
51
|
+
let blocked=false;
|
|
52
|
+
for(const hostname of candidateHostnames(text)){if(findDeniedDomainRule(networkPolicy,hostname)){blocked=true;break;}}
|
|
53
|
+
if(!blocked)return result;
|
|
54
|
+
const score=Math.min(100,result.score+blockedDomainSignal.weight);
|
|
55
|
+
return Object.freeze({...result,score,level:levelForScore(score),matches:Object.freeze([...result.matches,blockedDomainSignal])});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function scamSafetyGuidance(result){
|
|
59
|
+
if(result.level==='critical'||result.level==='high')return 'Stop contact. Do not send money, codes, or personal information. Verify independently and ask a trusted person for help.';
|
|
60
|
+
if(result.level==='caution')return 'Pause and verify the sender using contact information you already trust.';
|
|
61
|
+
return 'No strong warning signs were detected, but automated checks can miss scams. Stay cautious.';
|
|
62
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalApplicationId,
|
|
3
|
+
openApplicationDataDirectory
|
|
4
|
+
} from './AppDataScope.js';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_ENTRY_BYTES=4*1024*1024;
|
|
7
|
+
const MAX_NAMESPACE_LENGTH=96;
|
|
8
|
+
const MAX_KEY_LENGTH=240;
|
|
9
|
+
|
|
10
|
+
function safeSegment(value,label,maximum){
|
|
11
|
+
if(typeof value!=='string'){
|
|
12
|
+
throw new TypeError(`${label} must be a string.`);
|
|
13
|
+
}
|
|
14
|
+
const normalized=value.trim();
|
|
15
|
+
if(!normalized||normalized.length>maximum){
|
|
16
|
+
throw new RangeError(`${label} must contain between 1 and ${maximum} characters.`);
|
|
17
|
+
}
|
|
18
|
+
if(normalized==='.'||normalized==='..'||/[\\/\0]/u.test(normalized)){
|
|
19
|
+
throw new TypeError(`${label} must be one filename-safe segment.`);
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function positiveInteger(value,label,maximum){
|
|
25
|
+
if(!Number.isSafeInteger(value)||value<1||value>maximum){
|
|
26
|
+
throw new RangeError(`${label} must be an integer between 1 and ${maximum}.`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function unavailable(){
|
|
32
|
+
const error=new Error('Origin Private File System storage is unavailable in this browser.');
|
|
33
|
+
error.code='OPFS_UNAVAILABLE';
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function entryTooLarge(maximum){
|
|
38
|
+
const error=new RangeError(`The cache entry exceeds the ${maximum}-byte limit.`);
|
|
39
|
+
error.code='OPFS_CACHE_ENTRY_TOO_LARGE';
|
|
40
|
+
return error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A deliberately narrow JSON cache over one application-owned OPFS directory.
|
|
45
|
+
*
|
|
46
|
+
* It exposes only exact-key get, set, and delete operations. It never enumerates,
|
|
47
|
+
* exports, restores, clears, or selects another namespace, so one consumer cannot
|
|
48
|
+
* accidentally mutate unrelated origin storage.
|
|
49
|
+
*/
|
|
50
|
+
export default class ScopedOPFSCache{
|
|
51
|
+
#applicationId;
|
|
52
|
+
#arcane;
|
|
53
|
+
#directoryPromise=null;
|
|
54
|
+
#documentObject;
|
|
55
|
+
#maxEntryBytes;
|
|
56
|
+
#namespace;
|
|
57
|
+
#storage;
|
|
58
|
+
|
|
59
|
+
constructor({
|
|
60
|
+
applicationId=null,
|
|
61
|
+
namespace,
|
|
62
|
+
maxEntryBytes=DEFAULT_MAX_ENTRY_BYTES,
|
|
63
|
+
storage=globalThis.navigator?.storage,
|
|
64
|
+
documentObject=globalThis.document,
|
|
65
|
+
arcane=globalThis.Arcane
|
|
66
|
+
}={}){
|
|
67
|
+
this.#applicationId=applicationId==null||applicationId===''
|
|
68
|
+
?null
|
|
69
|
+
:canonicalApplicationId(applicationId);
|
|
70
|
+
this.#namespace=safeSegment(namespace,'namespace',MAX_NAMESPACE_LENGTH);
|
|
71
|
+
this.#maxEntryBytes=positiveInteger(maxEntryBytes,'maxEntryBytes',64*1024*1024);
|
|
72
|
+
if(!storage||typeof storage.getDirectory!=='function'){
|
|
73
|
+
throw unavailable();
|
|
74
|
+
}
|
|
75
|
+
this.#storage=storage;
|
|
76
|
+
this.#documentObject=documentObject;
|
|
77
|
+
this.#arcane=arcane;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static supported(storage=globalThis.navigator?.storage){
|
|
81
|
+
return Boolean(storage&&typeof storage.getDirectory==='function');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get namespace(){
|
|
85
|
+
return this.#namespace;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get applicationId(){
|
|
89
|
+
return this.#applicationId;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get maxEntryBytes(){
|
|
93
|
+
return this.#maxEntryBytes;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async #directory(){
|
|
97
|
+
if(!this.#directoryPromise){
|
|
98
|
+
this.#directoryPromise=(async()=>{
|
|
99
|
+
const scope=await openApplicationDataDirectory({
|
|
100
|
+
storage:this.#storage,
|
|
101
|
+
applicationId:this.#applicationId,
|
|
102
|
+
documentObject:this.#documentObject,
|
|
103
|
+
arcane:this.#arcane,
|
|
104
|
+
create:true
|
|
105
|
+
});
|
|
106
|
+
this.#applicationId=scope.applicationId;
|
|
107
|
+
return scope.directory.getDirectoryHandle(
|
|
108
|
+
this.#namespace,
|
|
109
|
+
{create:true}
|
|
110
|
+
);
|
|
111
|
+
})().catch(error=>{
|
|
112
|
+
this.#directoryPromise=null;
|
|
113
|
+
throw error;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return this.#directoryPromise;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async get(key){
|
|
120
|
+
const normalized=safeSegment(key,'key',MAX_KEY_LENGTH);
|
|
121
|
+
try{
|
|
122
|
+
const directory=await this.#directory();
|
|
123
|
+
const handle=await directory.getFileHandle(normalized);
|
|
124
|
+
const file=await handle.getFile();
|
|
125
|
+
if(file.size>this.#maxEntryBytes){
|
|
126
|
+
await this.delete(normalized).catch(()=>{});
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const source=await file.text();
|
|
130
|
+
if(new TextEncoder().encode(source).byteLength>this.#maxEntryBytes){
|
|
131
|
+
await this.delete(normalized).catch(()=>{});
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
try{
|
|
135
|
+
return JSON.parse(source);
|
|
136
|
+
}catch{
|
|
137
|
+
await this.delete(normalized).catch(()=>{});
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
}catch(error){
|
|
141
|
+
if(error?.name==='NotFoundError'||error?.code==='ENOENT'){
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async set(key,value){
|
|
149
|
+
const normalized=safeSegment(key,'key',MAX_KEY_LENGTH);
|
|
150
|
+
const source=JSON.stringify(value);
|
|
151
|
+
if(source===undefined){
|
|
152
|
+
throw new TypeError('Cache values must be JSON serializable.');
|
|
153
|
+
}
|
|
154
|
+
if(new TextEncoder().encode(source).byteLength>this.#maxEntryBytes){
|
|
155
|
+
throw entryTooLarge(this.#maxEntryBytes);
|
|
156
|
+
}
|
|
157
|
+
const directory=await this.#directory();
|
|
158
|
+
const handle=await directory.getFileHandle(normalized,{create:true});
|
|
159
|
+
const writable=await handle.createWritable();
|
|
160
|
+
try{
|
|
161
|
+
await writable.write(source);
|
|
162
|
+
await writable.close();
|
|
163
|
+
}catch(error){
|
|
164
|
+
await writable.abort?.().catch(()=>{});
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async delete(key){
|
|
171
|
+
const normalized=safeSegment(key,'key',MAX_KEY_LENGTH);
|
|
172
|
+
const directory=await this.#directory();
|
|
173
|
+
try{
|
|
174
|
+
await directory.removeEntry(normalized);
|
|
175
|
+
return true;
|
|
176
|
+
}catch(error){
|
|
177
|
+
if(error?.name==='NotFoundError'||error?.code==='ENOENT'){
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import GifEncoder from './GifEncoder.js';
|
|
2
|
+
|
|
3
|
+
function captureEvent(type,detail){return new CustomEvent(`capture-${type}`,{detail});}
|
|
4
|
+
function canvasBlob(canvas,type,quality){return new Promise((resolve,reject)=>canvas.toBlob(blob=>blob?resolve(blob):reject(new Error('Unable to encode the captured image.')),type,quality));}
|
|
5
|
+
function supportedRecorderType(Recorder){for(const type of ['video/webm;codecs=vp9,opus','video/webm;codecs=vp8,opus','video/webm','video/mp4'])if(Recorder.isTypeSupported?.(type))return type;return '';}
|
|
6
|
+
|
|
7
|
+
export default class ScreenCapture extends EventTarget{
|
|
8
|
+
constructor({mediaDevices=globalThis.navigator?.mediaDevices,Recorder=globalThis.MediaRecorder,documentRef=globalThis.document}={}){super();this.mediaDevices=mediaDevices;this.Recorder=Recorder;this.document=documentRef;this.mode='idle';this.stream=null;this.recorder=null;this.startedAt=0;this.frames=[];this.timer=0;this.video=null;this.canvas=null;this.stopPromise=null;}
|
|
9
|
+
available(){return Boolean(this.mediaDevices?.getDisplayMedia&&this.document);}
|
|
10
|
+
async acquire({audio=false}={}){if(!this.available())throw new Error('Display capture is unavailable in this browser.');return this.mediaDevices.getDisplayMedia({video:{frameRate:{ideal:15,max:30}},audio:Boolean(audio)});}
|
|
11
|
+
async captureImage({maxWidth=1920,type='image/png'}={}){this.assertIdle();this.emit('requesting',{mode:'image'});let stream;try{stream=await this.acquire();const {video,canvas}=await this.prepare(stream,maxWidth);canvas.getContext('2d').drawImage(video,0,0,canvas.width,canvas.height);const blob=await canvasBlob(canvas,type);const result={blob,mimeType:type,extension:type==='image/jpeg'?'jpg':'png',duration:0,width:canvas.width,height:canvas.height};this.emit('result',result);return result}catch(error){this.emit('error',{error,mode:'image'});throw error}finally{this.stopTracks(stream)}}
|
|
12
|
+
async startVideo({audio=true}={}){this.assertIdle();if(!this.Recorder)throw new Error('Video recording is unavailable in this browser.');this.emit('requesting',{mode:'video'});this.stream=await this.acquire({audio});const mimeType=supportedRecorderType(this.Recorder);const chunks=[];this.recorder=new this.Recorder(this.stream,mimeType?{mimeType}:undefined);this.stopPromise=new Promise((resolve,reject)=>{this.recorder.addEventListener('dataavailable',event=>{if(event.data?.size)chunks.push(event.data)});this.recorder.addEventListener('stop',()=>{const type=this.recorder.mimeType||mimeType||'video/webm';resolve({blob:new Blob(chunks,{type}),mimeType:type,extension:type.includes('mp4')?'mp4':'webm',duration:Date.now()-this.startedAt})},{once:true});this.recorder.addEventListener('error',event=>reject(event.error||new Error('Video recording failed.')),{once:true})});this.mode='video';this.startedAt=Date.now();this.recorder.start(500);this.emit('start',{mode:'video'});return true;}
|
|
13
|
+
async startGif({maxWidth=640,frameDelay=250}={}){this.assertIdle();this.emit('requesting',{mode:'gif'});this.stream=await this.acquire();const prepared=await this.prepare(this.stream,maxWidth);this.video=prepared.video;this.canvas=prepared.canvas;this.frames=[];this.mode='gif';this.startedAt=Date.now();const context=this.canvas.getContext('2d',{willReadFrequently:true});const sample=()=>{context.drawImage(this.video,0,0,this.canvas.width,this.canvas.height);this.frames.push(context.getImageData(0,0,this.canvas.width,this.canvas.height))};sample();this.timer=setInterval(sample,Math.max(100,frameDelay));this.gifDelay=Math.max(100,frameDelay);this.emit('start',{mode:'gif'});return true;}
|
|
14
|
+
async stop(){if(this.mode==='idle')return null;const mode=this.mode;try{let result;if(mode==='video'){this.recorder.stop();result=await this.stopPromise}else{clearInterval(this.timer);const encoder=new GifEncoder(this.canvas.width,this.canvas.height);for(const frame of this.frames)encoder.addFrame(frame,{delay:this.gifDelay});result={blob:encoder.encode(),mimeType:'image/gif',extension:'gif',duration:Date.now()-this.startedAt,width:this.canvas.width,height:this.canvas.height}}this.emit('result',result);return result}catch(error){this.emit('error',{error,mode});throw error}finally{this.stopTracks(this.stream);this.reset()}}
|
|
15
|
+
assertIdle(){if(this.mode!=='idle')throw new Error('Stop the active capture before starting another one.');}
|
|
16
|
+
async prepare(stream,maxWidth){const video=this.document.createElement('video');video.muted=true;video.playsInline=true;video.srcObject=stream;await new Promise((resolve,reject)=>{video.addEventListener('loadedmetadata',resolve,{once:true});video.addEventListener('error',()=>reject(new Error('Unable to read the selected display.')),{once:true})});await video.play();const scale=Math.min(1,Math.max(1,Number(maxWidth)||1920)/video.videoWidth);const canvas=this.document.createElement('canvas');canvas.width=Math.max(1,Math.round(video.videoWidth*scale));canvas.height=Math.max(1,Math.round(video.videoHeight*scale));return {video,canvas};}
|
|
17
|
+
stopTracks(stream){for(const track of stream?.getTracks?.()||[])track.stop()}
|
|
18
|
+
reset(){clearInterval(this.timer);this.mode='idle';this.stream=null;this.recorder=null;this.frames=[];this.timer=0;this.video=null;this.canvas=null;this.stopPromise=null;this.startedAt=0;this.emit('stop',{});}
|
|
19
|
+
emit(type,detail){this.dispatchEvent(captureEvent(type,detail));}
|
|
20
|
+
}
|