sh3-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Shell.svelte +185 -0
- package/dist/Shell.svelte.d.ts +4 -0
- package/dist/api.d.ts +22 -0
- package/dist/api.js +45 -0
- package/dist/apps/lifecycle.d.ts +37 -0
- package/dist/apps/lifecycle.js +153 -0
- package/dist/apps/registry.svelte.d.ts +37 -0
- package/dist/apps/registry.svelte.js +60 -0
- package/dist/apps/types.d.ts +61 -0
- package/dist/apps/types.js +10 -0
- package/dist/assets/icons.svg +1119 -0
- package/dist/auth/auth.svelte.d.ts +44 -0
- package/dist/auth/auth.svelte.js +119 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.js +1 -0
- package/dist/build.d.ts +29 -0
- package/dist/build.js +85 -0
- package/dist/contract.d.ts +20 -0
- package/dist/contract.js +28 -0
- package/dist/documents/backends.d.ts +17 -0
- package/dist/documents/backends.js +156 -0
- package/dist/documents/config.d.ts +7 -0
- package/dist/documents/config.js +27 -0
- package/dist/documents/handle.d.ts +6 -0
- package/dist/documents/handle.js +154 -0
- package/dist/documents/http-backend.d.ts +22 -0
- package/dist/documents/http-backend.js +78 -0
- package/dist/documents/index.d.ts +6 -0
- package/dist/documents/index.js +8 -0
- package/dist/documents/notifications.d.ts +9 -0
- package/dist/documents/notifications.js +39 -0
- package/dist/documents/types.d.ts +97 -0
- package/dist/documents/types.js +12 -0
- package/dist/host-entry.d.ts +9 -0
- package/dist/host-entry.js +15 -0
- package/dist/host.d.ts +13 -0
- package/dist/host.js +73 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/layout/DragPreview.svelte +63 -0
- package/dist/layout/DragPreview.svelte.d.ts +3 -0
- package/dist/layout/LayoutRenderer.svelte +260 -0
- package/dist/layout/LayoutRenderer.svelte.d.ts +6 -0
- package/dist/layout/SlotContainer.svelte +140 -0
- package/dist/layout/SlotContainer.svelte.d.ts +8 -0
- package/dist/layout/SlotDropZone.svelte +122 -0
- package/dist/layout/SlotDropZone.svelte.d.ts +8 -0
- package/dist/layout/drag.svelte.d.ts +45 -0
- package/dist/layout/drag.svelte.js +191 -0
- package/dist/layout/inspection.d.ts +52 -0
- package/dist/layout/inspection.js +157 -0
- package/dist/layout/ops.d.ts +78 -0
- package/dist/layout/ops.js +281 -0
- package/dist/layout/slotHostPool.svelte.d.ts +36 -0
- package/dist/layout/slotHostPool.svelte.js +229 -0
- package/dist/layout/store.svelte.d.ts +39 -0
- package/dist/layout/store.svelte.js +150 -0
- package/dist/layout/tree-walk.d.ts +15 -0
- package/dist/layout/tree-walk.js +33 -0
- package/dist/layout/types.d.ts +108 -0
- package/dist/layout/types.js +25 -0
- package/dist/overlays/ModalFrame.svelte +87 -0
- package/dist/overlays/ModalFrame.svelte.d.ts +10 -0
- package/dist/overlays/PopupFrame.svelte +85 -0
- package/dist/overlays/PopupFrame.svelte.d.ts +10 -0
- package/dist/overlays/ToastItem.svelte +77 -0
- package/dist/overlays/ToastItem.svelte.d.ts +9 -0
- package/dist/overlays/focusTrap.d.ts +1 -0
- package/dist/overlays/focusTrap.js +64 -0
- package/dist/overlays/modal.d.ts +9 -0
- package/dist/overlays/modal.js +141 -0
- package/dist/overlays/popup.d.ts +9 -0
- package/dist/overlays/popup.js +108 -0
- package/dist/overlays/roots.d.ts +4 -0
- package/dist/overlays/roots.js +31 -0
- package/dist/overlays/toast.d.ts +6 -0
- package/dist/overlays/toast.js +93 -0
- package/dist/overlays/types.d.ts +31 -0
- package/dist/overlays/types.js +15 -0
- package/dist/primitives/.gitkeep +0 -0
- package/dist/primitives/ResizableSplitter.svelte +333 -0
- package/dist/primitives/ResizableSplitter.svelte.d.ts +35 -0
- package/dist/primitives/TabbedPanel.svelte +305 -0
- package/dist/primitives/TabbedPanel.svelte.d.ts +50 -0
- package/dist/registry/client.d.ts +74 -0
- package/dist/registry/client.js +118 -0
- package/dist/registry/index.d.ts +13 -0
- package/dist/registry/index.js +14 -0
- package/dist/registry/installer.d.ts +53 -0
- package/dist/registry/installer.js +170 -0
- package/dist/registry/integrity.d.ts +32 -0
- package/dist/registry/integrity.js +92 -0
- package/dist/registry/loader.d.ts +50 -0
- package/dist/registry/loader.js +145 -0
- package/dist/registry/schema.d.ts +47 -0
- package/dist/registry/schema.js +180 -0
- package/dist/registry/storage.d.ts +37 -0
- package/dist/registry/storage.js +101 -0
- package/dist/registry/types.d.ts +245 -0
- package/dist/registry/types.js +14 -0
- package/dist/registry-shard/RegistryView.svelte +561 -0
- package/dist/registry-shard/RegistryView.svelte.d.ts +3 -0
- package/dist/registry-shard/registryApp.d.ts +10 -0
- package/dist/registry-shard/registryApp.js +24 -0
- package/dist/registry-shard/registryShard.svelte.d.ts +45 -0
- package/dist/registry-shard/registryShard.svelte.js +125 -0
- package/dist/shards/activate.svelte.d.ts +45 -0
- package/dist/shards/activate.svelte.js +124 -0
- package/dist/shards/registry.d.ts +4 -0
- package/dist/shards/registry.js +28 -0
- package/dist/shards/types.d.ts +155 -0
- package/dist/shards/types.js +20 -0
- package/dist/shell-shard/ShellHome.svelte +285 -0
- package/dist/shell-shard/ShellHome.svelte.d.ts +3 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +2 -0
- package/dist/shell-shard/shellShard.svelte.js +47 -0
- package/dist/shellRuntime.svelte.d.ts +27 -0
- package/dist/shellRuntime.svelte.js +27 -0
- package/dist/state/backends.d.ts +26 -0
- package/dist/state/backends.js +99 -0
- package/dist/state/types.d.ts +38 -0
- package/dist/state/types.js +15 -0
- package/dist/state/zones.svelte.d.ts +52 -0
- package/dist/state/zones.svelte.js +141 -0
- package/dist/store/InstalledView.svelte +201 -0
- package/dist/store/InstalledView.svelte.d.ts +3 -0
- package/dist/store/StoreView.svelte +470 -0
- package/dist/store/StoreView.svelte.d.ts +3 -0
- package/dist/store/storeApp.d.ts +11 -0
- package/dist/store/storeApp.js +26 -0
- package/dist/store/storeShard.svelte.d.ts +29 -0
- package/dist/store/storeShard.svelte.js +99 -0
- package/dist/tokens.css +79 -0
- package/package.json +50 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime loader -- loads a pre-built ESM bundle from raw bytes via blob URL.
|
|
3
|
+
*
|
|
4
|
+
* The install API hands us an ArrayBuffer (the verified bundle), we wrap it
|
|
5
|
+
* in a blob URL, dynamic-import it, then immediately revoke the URL so the
|
|
6
|
+
* blob can be GC'd. The module's default export must carry a `manifest`
|
|
7
|
+
* field; we use duck-typing to distinguish shards from apps.
|
|
8
|
+
*
|
|
9
|
+
* Bare specifier rewriting: bundles are built with external dependencies
|
|
10
|
+
* (`sh3`, `svelte`, `svelte/internal/client`, etc.). Blob URLs can't
|
|
11
|
+
* resolve bare specifiers, so we expose each dependency's runtime exports
|
|
12
|
+
* at a stable blob URL (a "shim") and rewrite bare specifiers in the
|
|
13
|
+
* bundle source before loading.
|
|
14
|
+
*/
|
|
15
|
+
import * as api from '../api';
|
|
16
|
+
import * as svelte from 'svelte';
|
|
17
|
+
// @ts-expect-error — svelte/internal/client has no public type declarations
|
|
18
|
+
import * as svelteInternalClient from 'svelte/internal/client';
|
|
19
|
+
// ---- shim infrastructure ---------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Create a blob URL that re-exports every runtime key from a module
|
|
22
|
+
* previously stashed on globalThis. The shim is created once and never
|
|
23
|
+
* revoked — it must outlive every bundle that references it.
|
|
24
|
+
*/
|
|
25
|
+
// JS reserved words that can't appear in destructuring or as bare export names.
|
|
26
|
+
const RESERVED = new Set([
|
|
27
|
+
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|
|
28
|
+
'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false',
|
|
29
|
+
'finally', 'for', 'function', 'if', 'import', 'in', 'instanceof', 'let',
|
|
30
|
+
'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try',
|
|
31
|
+
'typeof', 'var', 'void', 'while', 'with', 'yield',
|
|
32
|
+
]);
|
|
33
|
+
function createShimUrl(globalKey, mod) {
|
|
34
|
+
const names = Object.keys(mod);
|
|
35
|
+
const safe = names.filter(n => !RESERVED.has(n));
|
|
36
|
+
const reserved = names.filter(n => RESERVED.has(n));
|
|
37
|
+
const lines = [`const __m__ = globalThis.${globalKey};`];
|
|
38
|
+
// Safe names can use destructuring.
|
|
39
|
+
if (safe.length > 0) {
|
|
40
|
+
lines.push(`export const { ${safe.join(', ')} } = __m__;`);
|
|
41
|
+
}
|
|
42
|
+
// Reserved words need individual re-exports via a renamed binding.
|
|
43
|
+
for (const name of reserved) {
|
|
44
|
+
lines.push(`const __r_${name}__ = __m__['${name}']; export { __r_${name}__ as ${name} };`);
|
|
45
|
+
}
|
|
46
|
+
lines.push(`export default __m__.default;`);
|
|
47
|
+
return URL.createObjectURL(new Blob([lines.join('\n')], { type: 'application/javascript' }));
|
|
48
|
+
}
|
|
49
|
+
// Expose modules on globalThis so shims can read them.
|
|
50
|
+
const g = globalThis;
|
|
51
|
+
g.__sh3__ = api;
|
|
52
|
+
g.__sh3_svelte__ = svelte;
|
|
53
|
+
g.__sh3_svelte_internal_client__ = svelteInternalClient;
|
|
54
|
+
// Build shim URLs once at module load time.
|
|
55
|
+
const shimUrls = {
|
|
56
|
+
'sh3-core': createShimUrl('__sh3__', api),
|
|
57
|
+
'svelte': createShimUrl('__sh3_svelte__', svelte),
|
|
58
|
+
'svelte/internal/client': createShimUrl('__sh3_svelte_internal_client__', svelteInternalClient),
|
|
59
|
+
};
|
|
60
|
+
// `svelte/internal/disclose-version` is a side-effect-only import (no exports).
|
|
61
|
+
// Create an empty shim so the import doesn't fail.
|
|
62
|
+
shimUrls['svelte/internal/disclose-version'] = URL.createObjectURL(new Blob([''], { type: 'application/javascript' }));
|
|
63
|
+
// `sh3/tokens.css` — design tokens are already loaded by the host shell.
|
|
64
|
+
// Bundles that import this path get a no-op so the import doesn't fail.
|
|
65
|
+
shimUrls['sh3-core/tokens.css'] = URL.createObjectURL(new Blob([''], { type: 'application/javascript' }));
|
|
66
|
+
// ---- bare specifier rewriting ----------------------------------------------
|
|
67
|
+
/**
|
|
68
|
+
* Match bare specifier imports/re-exports:
|
|
69
|
+
* from 'svelte/internal/client'
|
|
70
|
+
* from "sh3-core"
|
|
71
|
+
* import "svelte/internal/disclose-version"
|
|
72
|
+
*
|
|
73
|
+
* Captures the quote char and the specifier. Only rewrites specifiers
|
|
74
|
+
* that have a shim URL registered above.
|
|
75
|
+
*/
|
|
76
|
+
const BARE_IMPORT_RE = /(?:from|import)\s*(['"])(sh3-core(?:\/[^'"]*)?|svelte(?:\/[^'"]*)?)\1/g;
|
|
77
|
+
function rewriteBareImports(source) {
|
|
78
|
+
return source.replace(BARE_IMPORT_RE, (match, _quote, specifier) => {
|
|
79
|
+
const shim = shimUrls[specifier];
|
|
80
|
+
if (!shim)
|
|
81
|
+
return match; // unknown specifier — leave as-is
|
|
82
|
+
// Preserve the keyword (from vs import) from the original match.
|
|
83
|
+
const keyword = match.startsWith('from') ? 'from' : 'import';
|
|
84
|
+
return `${keyword} '${shim}'`;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Load a pre-built ESM bundle from raw bytes.
|
|
89
|
+
*
|
|
90
|
+
* Scans all module exports (default + named) for objects with a `manifest`
|
|
91
|
+
* property and classifies each as a shard or app. Combo bundles that export
|
|
92
|
+
* both a shard and its companion app are supported.
|
|
93
|
+
*
|
|
94
|
+
* @param bytes - Raw bundle bytes (the verified ESM file contents).
|
|
95
|
+
* @returns All shards and apps found in the module's exports.
|
|
96
|
+
* @throws If the blob URL cannot be created or the import fails.
|
|
97
|
+
*/
|
|
98
|
+
export async function loadBundleModule(bytes) {
|
|
99
|
+
const source = new TextDecoder().decode(bytes);
|
|
100
|
+
const rewritten = rewriteBareImports(source);
|
|
101
|
+
const blob = new Blob([rewritten], { type: 'application/javascript' });
|
|
102
|
+
const url = URL.createObjectURL(blob);
|
|
103
|
+
try {
|
|
104
|
+
const mod = await import(/* @vite-ignore */ url);
|
|
105
|
+
const result = { shards: [], apps: [] };
|
|
106
|
+
for (const value of Object.values(mod)) {
|
|
107
|
+
if (!value || typeof value !== 'object' || !('manifest' in value)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const candidate = value;
|
|
111
|
+
if (isShard(candidate))
|
|
112
|
+
result.shards.push(candidate);
|
|
113
|
+
else if (isApp(candidate))
|
|
114
|
+
result.apps.push(candidate);
|
|
115
|
+
}
|
|
116
|
+
if (result.shards.length === 0 && result.apps.length === 0) {
|
|
117
|
+
throw new Error('Bundle has no exports with a "manifest" property — expected at least one shard or app');
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
URL.revokeObjectURL(url);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Type guard: returns true if the loaded module is a shard.
|
|
127
|
+
*
|
|
128
|
+
* A shard has an `activate` function and `manifest.views` array. An app
|
|
129
|
+
* has neither — it has `initialLayout` and `manifest.requiredShards`.
|
|
130
|
+
*/
|
|
131
|
+
export function isShard(mod) {
|
|
132
|
+
return ('activate' in mod &&
|
|
133
|
+
typeof mod.activate === 'function' &&
|
|
134
|
+
Array.isArray(mod.manifest.views));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Type guard: returns true if the loaded module is an app.
|
|
138
|
+
*
|
|
139
|
+
* An app has `initialLayout` and `manifest.requiredShards`. A shard has
|
|
140
|
+
* `activate` and `manifest.views` instead.
|
|
141
|
+
*/
|
|
142
|
+
export function isApp(mod) {
|
|
143
|
+
return ('initialLayout' in mod &&
|
|
144
|
+
Array.isArray(mod.manifest.requiredShards));
|
|
145
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime validation for registry index JSON.
|
|
3
|
+
*
|
|
4
|
+
* No external schema library — plain runtime checks. The registry index
|
|
5
|
+
* is fetched from untrusted URLs so all fields are validated before any
|
|
6
|
+
* typed access occurs. Validation errors carry a JSON-path string so the
|
|
7
|
+
* caller can surface actionable diagnostics in the store UI.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ```ts
|
|
11
|
+
* const raw = await response.json();
|
|
12
|
+
* const index = validateRegistryIndex(raw); // throws RegistryValidationError on bad input
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
import type { RegistryIndex } from './types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when a registry index document fails validation.
|
|
18
|
+
*
|
|
19
|
+
* The `path` property is a JSON-path string pointing to the field that
|
|
20
|
+
* caused the failure (e.g. `"$.packages[2].versions[0].integrity"`).
|
|
21
|
+
* Consumers may display this directly in diagnostic/error UI.
|
|
22
|
+
*/
|
|
23
|
+
export declare class RegistryValidationError extends Error {
|
|
24
|
+
/**
|
|
25
|
+
* JSON-path of the failing field (e.g. `"$.packages[0].type"`).
|
|
26
|
+
*/
|
|
27
|
+
readonly path: string;
|
|
28
|
+
/**
|
|
29
|
+
* @param path - JSON-path string of the failing field.
|
|
30
|
+
* @param message - Human-readable description of why validation failed.
|
|
31
|
+
*/
|
|
32
|
+
constructor(path: string, message: string);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate an unknown value as a `RegistryIndex`.
|
|
36
|
+
*
|
|
37
|
+
* Performs a full deep validation of all required fields and their types.
|
|
38
|
+
* Returns a correctly typed `RegistryIndex` on success.
|
|
39
|
+
* Throws `RegistryValidationError` on the first validation failure.
|
|
40
|
+
*
|
|
41
|
+
* Call this immediately after `response.json()` before touching any field.
|
|
42
|
+
*
|
|
43
|
+
* @param data - The raw parsed JSON value from a registry fetch.
|
|
44
|
+
* @returns A fully validated `RegistryIndex`.
|
|
45
|
+
* @throws `RegistryValidationError` if any field is missing or has the wrong type.
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateRegistryIndex(data: unknown): RegistryIndex;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime validation for registry index JSON.
|
|
3
|
+
*
|
|
4
|
+
* No external schema library — plain runtime checks. The registry index
|
|
5
|
+
* is fetched from untrusted URLs so all fields are validated before any
|
|
6
|
+
* typed access occurs. Validation errors carry a JSON-path string so the
|
|
7
|
+
* caller can surface actionable diagnostics in the store UI.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* ```ts
|
|
11
|
+
* const raw = await response.json();
|
|
12
|
+
* const index = validateRegistryIndex(raw); // throws RegistryValidationError on bad input
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when a registry index document fails validation.
|
|
17
|
+
*
|
|
18
|
+
* The `path` property is a JSON-path string pointing to the field that
|
|
19
|
+
* caused the failure (e.g. `"$.packages[2].versions[0].integrity"`).
|
|
20
|
+
* Consumers may display this directly in diagnostic/error UI.
|
|
21
|
+
*/
|
|
22
|
+
export class RegistryValidationError extends Error {
|
|
23
|
+
/**
|
|
24
|
+
* @param path - JSON-path string of the failing field.
|
|
25
|
+
* @param message - Human-readable description of why validation failed.
|
|
26
|
+
*/
|
|
27
|
+
constructor(path, message) {
|
|
28
|
+
super(`Registry validation error at ${path}: ${message}`);
|
|
29
|
+
this.name = 'RegistryValidationError';
|
|
30
|
+
this.path = path;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validate an unknown value as a `RegistryIndex`.
|
|
35
|
+
*
|
|
36
|
+
* Performs a full deep validation of all required fields and their types.
|
|
37
|
+
* Returns a correctly typed `RegistryIndex` on success.
|
|
38
|
+
* Throws `RegistryValidationError` on the first validation failure.
|
|
39
|
+
*
|
|
40
|
+
* Call this immediately after `response.json()` before touching any field.
|
|
41
|
+
*
|
|
42
|
+
* @param data - The raw parsed JSON value from a registry fetch.
|
|
43
|
+
* @returns A fully validated `RegistryIndex`.
|
|
44
|
+
* @throws `RegistryValidationError` if any field is missing or has the wrong type.
|
|
45
|
+
*/
|
|
46
|
+
export function validateRegistryIndex(data) {
|
|
47
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
48
|
+
throw new RegistryValidationError('$', 'expected an object');
|
|
49
|
+
}
|
|
50
|
+
const obj = data;
|
|
51
|
+
if (obj['version'] !== 1) {
|
|
52
|
+
throw new RegistryValidationError('$.version', `expected 1, got ${JSON.stringify(obj['version'])}`);
|
|
53
|
+
}
|
|
54
|
+
if (!Array.isArray(obj['packages'])) {
|
|
55
|
+
throw new RegistryValidationError('$.packages', 'expected an array');
|
|
56
|
+
}
|
|
57
|
+
const packages = obj['packages'].map((p, i) => validatePackageEntry(p, `$.packages[${i}]`));
|
|
58
|
+
return { version: 1, packages };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Validate a single package entry object.
|
|
62
|
+
*
|
|
63
|
+
* @param data - Raw value from the `packages` array.
|
|
64
|
+
* @param path - JSON-path prefix used in error messages.
|
|
65
|
+
* @returns Validated `PackageEntry`.
|
|
66
|
+
* @throws `RegistryValidationError` on any field failure.
|
|
67
|
+
*/
|
|
68
|
+
function validatePackageEntry(data, path) {
|
|
69
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
70
|
+
throw new RegistryValidationError(path, 'expected an object');
|
|
71
|
+
}
|
|
72
|
+
const obj = data;
|
|
73
|
+
requireString(obj, 'id', path);
|
|
74
|
+
requireOneOf(obj, 'type', ['shard', 'app'], path);
|
|
75
|
+
requireString(obj, 'label', path);
|
|
76
|
+
requireString(obj, 'description', path);
|
|
77
|
+
// author: { name: string }
|
|
78
|
+
const authorPath = `${path}.author`;
|
|
79
|
+
if (!obj['author'] || typeof obj['author'] !== 'object' || Array.isArray(obj['author'])) {
|
|
80
|
+
throw new RegistryValidationError(authorPath, 'expected an object');
|
|
81
|
+
}
|
|
82
|
+
const author = obj['author'];
|
|
83
|
+
if (typeof author['name'] !== 'string' || author['name'].length === 0) {
|
|
84
|
+
throw new RegistryValidationError(`${authorPath}.name`, 'expected a non-empty string');
|
|
85
|
+
}
|
|
86
|
+
// versions: non-empty array
|
|
87
|
+
if (!Array.isArray(obj['versions']) || obj['versions'].length === 0) {
|
|
88
|
+
throw new RegistryValidationError(`${path}.versions`, 'expected a non-empty array');
|
|
89
|
+
}
|
|
90
|
+
const versions = obj['versions'].map((v, i) => validatePackageVersion(v, `${path}.versions[${i}]`));
|
|
91
|
+
return {
|
|
92
|
+
id: obj['id'],
|
|
93
|
+
type: obj['type'],
|
|
94
|
+
label: obj['label'],
|
|
95
|
+
description: obj['description'],
|
|
96
|
+
author: { name: author['name'] },
|
|
97
|
+
icon: typeof obj['icon'] === 'string' ? obj['icon'] : undefined,
|
|
98
|
+
versions,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Validate a single package version object.
|
|
103
|
+
*
|
|
104
|
+
* @param data - Raw value from a `versions` array.
|
|
105
|
+
* @param path - JSON-path prefix used in error messages.
|
|
106
|
+
* @returns Validated `PackageVersion`.
|
|
107
|
+
* @throws `RegistryValidationError` on any field failure.
|
|
108
|
+
*/
|
|
109
|
+
function validatePackageVersion(data, path) {
|
|
110
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
111
|
+
throw new RegistryValidationError(path, 'expected an object');
|
|
112
|
+
}
|
|
113
|
+
const obj = data;
|
|
114
|
+
requireString(obj, 'version', path);
|
|
115
|
+
requireString(obj, 'contractVersion', path);
|
|
116
|
+
requireString(obj, 'bundleUrl', path);
|
|
117
|
+
requireString(obj, 'integrity', path);
|
|
118
|
+
let requires;
|
|
119
|
+
if (obj['requires'] !== undefined) {
|
|
120
|
+
if (!Array.isArray(obj['requires'])) {
|
|
121
|
+
throw new RegistryValidationError(`${path}.requires`, 'expected an array');
|
|
122
|
+
}
|
|
123
|
+
requires = obj['requires'].map((r, i) => validateRequiredDependency(r, `${path}.requires[${i}]`));
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
version: obj['version'],
|
|
127
|
+
contractVersion: obj['contractVersion'],
|
|
128
|
+
bundleUrl: obj['bundleUrl'],
|
|
129
|
+
integrity: obj['integrity'],
|
|
130
|
+
requires,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Validate a single dependency entry in a `requires` array.
|
|
135
|
+
*
|
|
136
|
+
* @param data - Raw value from a `requires` array.
|
|
137
|
+
* @param path - JSON-path prefix used in error messages.
|
|
138
|
+
* @returns Validated `RequiredDependency`.
|
|
139
|
+
* @throws `RegistryValidationError` on any field failure.
|
|
140
|
+
*/
|
|
141
|
+
function validateRequiredDependency(data, path) {
|
|
142
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
143
|
+
throw new RegistryValidationError(path, 'expected an object');
|
|
144
|
+
}
|
|
145
|
+
const obj = data;
|
|
146
|
+
requireString(obj, 'id', path);
|
|
147
|
+
requireString(obj, 'versionRange', path);
|
|
148
|
+
return {
|
|
149
|
+
id: obj['id'],
|
|
150
|
+
versionRange: obj['versionRange'],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Assert that `obj[field]` is a non-empty string.
|
|
155
|
+
*
|
|
156
|
+
* @param obj - Parent object being validated.
|
|
157
|
+
* @param field - Field name to check.
|
|
158
|
+
* @param path - JSON-path of the parent object (field name is appended on error).
|
|
159
|
+
* @throws `RegistryValidationError` if the field is missing, not a string, or empty.
|
|
160
|
+
*/
|
|
161
|
+
function requireString(obj, field, path) {
|
|
162
|
+
const value = obj[field];
|
|
163
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
164
|
+
throw new RegistryValidationError(`${path}.${field}`, 'expected a non-empty string');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Assert that `obj[field]` is one of the allowed string values.
|
|
169
|
+
*
|
|
170
|
+
* @param obj - Parent object being validated.
|
|
171
|
+
* @param field - Field name to check.
|
|
172
|
+
* @param values - Allowed values.
|
|
173
|
+
* @param path - JSON-path of the parent object (field name is appended on error).
|
|
174
|
+
* @throws `RegistryValidationError` if the field value is not in `values`.
|
|
175
|
+
*/
|
|
176
|
+
function requireOneOf(obj, field, values, path) {
|
|
177
|
+
if (!values.includes(obj[field])) {
|
|
178
|
+
throw new RegistryValidationError(`${path}.${field}`, `expected one of: ${values.join(', ')}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package storage -- persists installed bundles and metadata.
|
|
3
|
+
*
|
|
4
|
+
* Uses IndexedDB, available in all target environments (browser, Tauri
|
|
5
|
+
* WebView, Android WebView). Two object stores:
|
|
6
|
+
* - "bundles" -- raw ArrayBuffer keyed by package id
|
|
7
|
+
* - "meta" -- InstalledPackage records keyed by package id
|
|
8
|
+
*
|
|
9
|
+
* Designed to be swappable: a Tauri target could later substitute a
|
|
10
|
+
* filesystem-backed implementation behind the same interface.
|
|
11
|
+
*/
|
|
12
|
+
import type { InstalledPackage } from './types';
|
|
13
|
+
/**
|
|
14
|
+
* Persist a package bundle and its metadata.
|
|
15
|
+
* Overwrites any existing record for the same `id`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function savePackage(id: string, bundle: ArrayBuffer, meta: InstalledPackage): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Load the raw bundle bytes for an installed package.
|
|
20
|
+
* Returns `null` if no bundle is stored for `id`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadBundle(id: string): Promise<ArrayBuffer | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Load the metadata record for an installed package.
|
|
25
|
+
* Returns `null` if no metadata is stored for `id`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadMeta(id: string): Promise<InstalledPackage | null>;
|
|
28
|
+
/**
|
|
29
|
+
* List metadata for all installed packages.
|
|
30
|
+
* Returns an empty array when nothing has been installed yet.
|
|
31
|
+
*/
|
|
32
|
+
export declare function listInstalled(): Promise<InstalledPackage[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Remove a package's bundle and metadata from storage.
|
|
35
|
+
* No-ops silently if the package was not installed.
|
|
36
|
+
*/
|
|
37
|
+
export declare function removePackage(id: string): Promise<void>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package storage -- persists installed bundles and metadata.
|
|
3
|
+
*
|
|
4
|
+
* Uses IndexedDB, available in all target environments (browser, Tauri
|
|
5
|
+
* WebView, Android WebView). Two object stores:
|
|
6
|
+
* - "bundles" -- raw ArrayBuffer keyed by package id
|
|
7
|
+
* - "meta" -- InstalledPackage records keyed by package id
|
|
8
|
+
*
|
|
9
|
+
* Designed to be swappable: a Tauri target could later substitute a
|
|
10
|
+
* filesystem-backed implementation behind the same interface.
|
|
11
|
+
*/
|
|
12
|
+
const DB_NAME = 'sh3-packages';
|
|
13
|
+
const DB_VERSION = 1;
|
|
14
|
+
const BUNDLES_STORE = 'bundles';
|
|
15
|
+
const META_STORE = 'meta';
|
|
16
|
+
/**
|
|
17
|
+
* Opens (or creates) the sh3-packages IndexedDB database.
|
|
18
|
+
* Called internally before every storage operation.
|
|
19
|
+
*/
|
|
20
|
+
function openDb() {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
23
|
+
request.onupgradeneeded = () => {
|
|
24
|
+
const db = request.result;
|
|
25
|
+
if (!db.objectStoreNames.contains(BUNDLES_STORE)) {
|
|
26
|
+
db.createObjectStore(BUNDLES_STORE);
|
|
27
|
+
}
|
|
28
|
+
if (!db.objectStoreNames.contains(META_STORE)) {
|
|
29
|
+
db.createObjectStore(META_STORE);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
request.onsuccess = () => resolve(request.result);
|
|
33
|
+
request.onerror = () => reject(request.error);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Wraps a single IDBObjectStore operation in a promise.
|
|
38
|
+
* Opens a transaction on `store`, calls `fn` with the object store, and
|
|
39
|
+
* resolves with the request result or rejects on error.
|
|
40
|
+
*/
|
|
41
|
+
function tx(db, store, mode, fn) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const transaction = db.transaction(store, mode);
|
|
44
|
+
const objectStore = transaction.objectStore(store);
|
|
45
|
+
const request = fn(objectStore);
|
|
46
|
+
request.onsuccess = () => resolve(request.result);
|
|
47
|
+
request.onerror = () => reject(request.error);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Persist a package bundle and its metadata.
|
|
52
|
+
* Overwrites any existing record for the same `id`.
|
|
53
|
+
*/
|
|
54
|
+
export async function savePackage(id, bundle, meta) {
|
|
55
|
+
const db = await openDb();
|
|
56
|
+
await tx(db, BUNDLES_STORE, 'readwrite', (s) => s.put(bundle, id));
|
|
57
|
+
await tx(db, META_STORE, 'readwrite', (s) => s.put(meta, id));
|
|
58
|
+
db.close();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Load the raw bundle bytes for an installed package.
|
|
62
|
+
* Returns `null` if no bundle is stored for `id`.
|
|
63
|
+
*/
|
|
64
|
+
export async function loadBundle(id) {
|
|
65
|
+
const db = await openDb();
|
|
66
|
+
const result = await tx(db, BUNDLES_STORE, 'readonly', (s) => s.get(id));
|
|
67
|
+
db.close();
|
|
68
|
+
return result !== null && result !== void 0 ? result : null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Load the metadata record for an installed package.
|
|
72
|
+
* Returns `null` if no metadata is stored for `id`.
|
|
73
|
+
*/
|
|
74
|
+
export async function loadMeta(id) {
|
|
75
|
+
var _a;
|
|
76
|
+
const db = await openDb();
|
|
77
|
+
const result = await tx(db, META_STORE, 'readonly', (s) => s.get(id));
|
|
78
|
+
db.close();
|
|
79
|
+
return (_a = result) !== null && _a !== void 0 ? _a : null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* List metadata for all installed packages.
|
|
83
|
+
* Returns an empty array when nothing has been installed yet.
|
|
84
|
+
*/
|
|
85
|
+
export async function listInstalled() {
|
|
86
|
+
var _a;
|
|
87
|
+
const db = await openDb();
|
|
88
|
+
const result = await tx(db, META_STORE, 'readonly', (s) => s.getAll());
|
|
89
|
+
db.close();
|
|
90
|
+
return (_a = result) !== null && _a !== void 0 ? _a : [];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Remove a package's bundle and metadata from storage.
|
|
94
|
+
* No-ops silently if the package was not installed.
|
|
95
|
+
*/
|
|
96
|
+
export async function removePackage(id) {
|
|
97
|
+
const db = await openDb();
|
|
98
|
+
await tx(db, BUNDLES_STORE, 'readwrite', (s) => s.delete(id));
|
|
99
|
+
await tx(db, META_STORE, 'readwrite', (s) => s.delete(id));
|
|
100
|
+
db.close();
|
|
101
|
+
}
|