sh3-core 0.20.3 → 0.21.2

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/api.d.ts CHANGED
@@ -40,9 +40,9 @@ export type { ColorPickOptions, ColorContribution, ColorApi, } from './color/api
40
40
  export { COLOR_PICKER_POINT } from './color/api';
41
41
  export { registeredShards, activeShards, erroredShards } from './shards/activate.svelte';
42
42
  export type { ShardErrorEntry } from './shards/activate.svelte';
43
- export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, } from './registry/types';
43
+ export type { RegistryIndex, PackageEntry, PackageVersion, RequiredDependency, InstalledPackage, InstallResult, PackageMeta, RemoteInstallRequest, } from './registry/types';
44
44
  export type { ResolvedPackage } from './registry/client';
45
- export { fetchRegistries, fetchBundle, buildPackageMeta } from './registry/client';
45
+ export { fetchRegistries, fetchArchive, buildPackageMeta } from './registry/client';
46
46
  export { validateRegistryIndex } from './registry/schema';
47
47
  export { PERMISSION_KEYS_MINT, ScopeEscalationError, ConsentDeniedError, type ShardContextKeys, type ApiKeyPublic, type MintOpts, } from './keys/types';
48
48
  export { registerConsentListener, resolveConsent, type ConsentRequest } from './keys/consent.svelte';
package/dist/api.js CHANGED
@@ -43,7 +43,7 @@ export { COLOR_PICKER_POINT } from './color/api';
43
43
  // addition: diagnostic used to reach `activate.svelte` directly via $lib;
44
44
  // the package boundary requires routing through the public surface.
45
45
  export { registeredShards, activeShards, erroredShards } from './shards/activate.svelte';
46
- export { fetchRegistries, fetchBundle, buildPackageMeta } from './registry/client';
46
+ export { fetchRegistries, fetchArchive, buildPackageMeta } from './registry/client';
47
47
  export { validateRegistryIndex } from './registry/schema';
48
48
  // Key mint/revoke types — client shards that declare `keys:mint` get ctx.keys.
49
49
  export { PERMISSION_KEYS_MINT, ScopeEscalationError, ConsentDeniedError, } from './keys/types';
@@ -7,11 +7,12 @@
7
7
  */
8
8
 
9
9
  import { storeContext } from './storeShard.svelte';
10
- import { fetchBundle, fetchServerBundle, buildPackageMeta } from '../../registry/client';
10
+ import { fetchArchive, buildPackageMeta } from '../../registry/client';
11
+ import { readFileFromArchive, readManifestFromArchive } from '../../registry/archive';
11
12
  import { installPackage } from '../../registry/installer';
12
13
  import { loadBundleModule, type LoadedBundle } from '../../registry/loader';
13
14
  import { extractBundlePermissions } from '../../registry/permission-descriptions';
14
- import { serverInstallPackage } from '../../env/client';
15
+ import { requestServerInstall } from '../../env/client';
15
16
  import { checkBundleFetch } from '../../registry/checkFetch';
16
17
  import { contract } from '../../contract';
17
18
  import type { ResolvedPackage } from '../../registry/client';
@@ -36,9 +37,8 @@
36
37
  pkg: ResolvedPackage;
37
38
  permissions: string[];
38
39
  loaded: LoadedBundle;
39
- bundle: ArrayBuffer;
40
+ archiveBytes: Uint8Array;
40
41
  meta: ReturnType<typeof buildPackageMeta>;
41
- serverBundle: ArrayBuffer | undefined;
42
42
  warnings: string[];
43
43
  }>(null);
44
44
 
@@ -111,6 +111,8 @@
111
111
  for (const p of installed) {
112
112
  if (p.type === 'shard' || p.type === 'combo') known.add(p.id);
113
113
  }
114
+ // A combo package provides its own shard — don't block it on its own id.
115
+ if (pkg.entry.type === 'combo') known.add(pkg.entry.id);
114
116
  return required.filter((id: string) => !known.has(id));
115
117
  }
116
118
 
@@ -171,26 +173,20 @@
171
173
  installError = null;
172
174
 
173
175
  try {
174
- // 1. Fetch and integrity-verify the client bundle from the registry.
175
- const bundle = await fetchBundle(pkg.latest, pkg.sourceRegistry);
176
+ // 1. Fetch and integrity-verify the archive from the registry.
177
+ const archiveBytes = await fetchArchive(pkg.latest, pkg.sourceRegistry);
176
178
  const meta = buildPackageMeta(pkg, pkg.latest);
177
179
 
178
- // 2. Load the module once, extract permissions for the confirmation
179
- // modal, and reuse the loaded reference on install.
180
- const loaded = await loadBundleModule(bundle);
181
- const permissions = extractBundlePermissions(loaded);
180
+ // 2. Extract client.js, load the module to get permissions for the modal.
181
+ const clientJs = readFileFromArchive(archiveBytes, 'client.js');
182
+ const loaded = clientJs ? await loadBundleModule(clientJs) : null;
183
+ const permissions = loaded ? extractBundlePermissions(loaded) : [];
182
184
 
183
- // 3. Fetch the server bundle upfront so the modal is the last blocker.
184
- let serverBundle: ArrayBuffer | undefined;
185
- if (pkg.latest.serverBundleUrl) {
186
- serverBundle = await fetchServerBundle(pkg.latest, pkg.sourceRegistry);
187
- }
188
-
189
- // 4. Show the confirmation modal. The actual install happens in
185
+ // 3. Show the confirmation modal. The actual install happens in
190
186
  // confirmInstall() once the user clicks Install.
191
- const bundleText = new TextDecoder().decode(new Uint8Array(bundle));
187
+ const bundleText = clientJs ? new TextDecoder().decode(new Uint8Array(clientJs)) : '';
192
188
  const warnings = checkBundleFetch(bundleText);
193
- installModal = { pkg, permissions, loaded, bundle, meta, serverBundle, warnings };
189
+ installModal = { pkg, permissions, loaded: loaded!, archiveBytes, meta, warnings };
194
190
  } catch (err) {
195
191
  installError = err instanceof Error ? err.message : String(err);
196
192
  const next = new Set(installingIds);
@@ -204,29 +200,26 @@
204
200
  if (!ctxModal) return;
205
201
  installModal = null;
206
202
 
207
- const { pkg, loaded, bundle, meta, serverBundle } = ctxModal;
203
+ const { pkg, loaded, archiveBytes, meta } = ctxModal;
208
204
  const id = pkg.entry.id;
209
205
 
210
206
  try {
211
- const manifest = {
212
- id: meta.id,
213
- type: meta.type,
214
- label: pkg.entry.label,
215
- version: meta.version,
216
- contractVersion: meta.contractVersion,
217
- sourceRegistry: meta.sourceRegistry,
218
- requiredShards: pkg.latest.requires?.map((r) => r.id) ?? [],
219
- installedAt: new Date().toISOString(),
220
- };
221
- const serverResult = await serverInstallPackage(manifest, bundle, serverBundle);
207
+ const serverResult = await requestServerInstall(pkg.sourceRegistry, pkg.entry.id, meta.version);
222
208
  if (!serverResult.ok) {
223
- installError = serverResult.error ?? 'Server install failed';
209
+ let errMsg = serverResult.error ?? 'Server install failed';
210
+ if (serverResult.code === 'missing-shards' && serverResult.missing) {
211
+ errMsg = `missing required shard(s): ${serverResult.missing.map((m: { id: string }) => m.id).join(', ')}`;
212
+ }
213
+ installError = errMsg;
224
214
  return;
225
215
  }
226
216
 
227
- const result = await installPackage(bundle, meta, { loaded });
228
- if (!result.success) {
229
- console.warn(`[sh3-store] Server install ok but local hot-load failed: ${result.error}`);
217
+ const clientJs = readFileFromArchive(archiveBytes, 'client.js');
218
+ if (clientJs) {
219
+ const result = await installPackage(clientJs, meta, { loaded });
220
+ if (!result.success) {
221
+ console.warn(`[sh3-store] Server install ok but local hot-load failed: ${result.error}`);
222
+ }
230
223
  }
231
224
 
232
225
  await ctx.refreshInstalled();
@@ -17,13 +17,14 @@
17
17
  */
18
18
  import { mount, unmount } from 'svelte';
19
19
  import StoreView from './StoreView.svelte';
20
- import { fetchRegistries, fetchBundle, fetchServerBundle, buildPackageMeta } from '../../registry/client';
20
+ import { fetchRegistries, fetchArchive, buildPackageMeta } from '../../registry/client';
21
21
  import { installPackage, listInstalledPackages } from '../../registry/installer';
22
22
  import { uninstallPackage as installerUninstallPackage } from '../../registry/installer';
23
23
  import { loadBundle, loadMeta, savePackage } from '../../registry/storage';
24
24
  import { loadBundleModule } from '../../registry/loader';
25
25
  import { extractBundlePermissions } from '../../registry/permission-descriptions';
26
- import { serverInstallPackage, fetchServerPackages, serverUninstallPackage } from '../../env/client';
26
+ import { readFileFromArchive } from '../../registry/archive';
27
+ import { requestServerInstall, fetchServerPackages, serverUninstallPackage } from '../../env/client';
27
28
  import { VERSION } from '../../version';
28
29
  import { installVerb, uninstallVerb, appinfoVerb, updateVerb } from './verbs';
29
30
  import { isNewerVersion } from './version';
@@ -147,7 +148,7 @@ export const storeShard = {
147
148
  await ctx.envUpdate({ registries });
148
149
  }
149
150
  async function updatePackage(id, confirmPermissionChange, version) {
150
- var _a, _b, _c, _d;
151
+ var _a, _b;
151
152
  // Source the catalog entry. Without an explicit version we use the
152
153
  // updatable map (which encodes the "newer than installed" check); with a
153
154
  // version we look up the package in the full catalog so downgrades and
@@ -165,77 +166,62 @@ export const storeShard = {
165
166
  if (!installedRecord)
166
167
  return;
167
168
  const picked = pickVersion(catalogEntry, version);
168
- // 1. Fetch new bundle(s).
169
- const bundle = await fetchBundle(picked, catalogEntry.sourceRegistry);
170
- let serverBundle;
171
- if (picked.serverBundleUrl) {
172
- serverBundle = await fetchServerBundle(picked, catalogEntry.sourceRegistry);
173
- }
169
+ // 1. Fetch archive (verified against SRI).
170
+ const archiveBytes = await fetchArchive(picked, catalogEntry.sourceRegistry);
174
171
  const meta = buildPackageMeta(catalogEntry, picked);
175
- // 2. Load the module once for permission extraction and install reuse.
176
- const loaded = await loadBundleModule(bundle);
177
- const newPerms = extractBundlePermissions(loaded);
178
- // 3. Look up the locally persisted old permissions (server-sourced
179
- // installed list doesn't carry permissions — per spec they live
180
- // in the local IndexedDB record).
172
+ // 2. Extract client.js for permission check and local install.
173
+ const clientJs = readFileFromArchive(archiveBytes, 'client.js');
174
+ // 3. Load module for permission extraction (if client bundle present).
175
+ let loaded;
176
+ let newPerms = [];
177
+ if (clientJs) {
178
+ loaded = await loadBundleModule(clientJs);
179
+ newPerms = extractBundlePermissions(loaded);
180
+ }
181
+ // 4. Look up the locally persisted old permissions.
181
182
  let oldPerms = [];
182
183
  try {
183
184
  const localMeta = await loadMeta(id);
184
185
  if (localMeta === null || localMeta === void 0 ? void 0 : localMeta.permissions)
185
186
  oldPerms = localMeta.permissions;
186
187
  }
187
- catch (_e) {
188
- // No local record (e.g. installed on a different browser); treat as
189
- // empty. The diff will show all new permissions as additions.
188
+ catch (_c) {
189
+ // No local record; treat as empty.
190
190
  }
191
- // 4. If the permission set changed and a confirmation callback was
192
- // provided, await the user's decision before touching the server.
191
+ // 5. If the permission set changed, await the user's decision.
193
192
  const { added, removed } = diffPermissions(oldPerms, newPerms);
194
193
  if ((added.length > 0 || removed.length > 0) && confirmPermissionChange) {
195
194
  const ok = await confirmPermissionChange(added, removed);
196
195
  if (!ok)
197
196
  return;
198
197
  }
199
- // 5. Snapshot current state for rollback. Preserve the locally-known
200
- // permissions so the rollback write still satisfies the InstalledPackage
201
- // contract (installedRecord came from the server-sourced list which
202
- // lacks permissions).
198
+ // 6. Snapshot current state for rollback.
203
199
  const oldBundle = await loadBundle(id);
204
200
  const oldRecord = Object.assign(Object.assign({}, installedRecord), { permissions: oldPerms });
205
- // 6. Push to server.
206
- const manifest = {
207
- id: meta.id,
208
- type: meta.type,
209
- label: catalogEntry.entry.label,
210
- version: meta.version,
211
- contractVersion: meta.contractVersion,
212
- sourceRegistry: meta.sourceRegistry,
213
- installedAt: new Date().toISOString(),
214
- requiredShards: (_b = (_a = picked.requires) === null || _a === void 0 ? void 0 : _a.map((r) => r.id)) !== null && _b !== void 0 ? _b : [],
215
- };
216
- const serverResult = await serverInstallPackage(manifest, bundle, serverBundle);
201
+ // 7. Tell server to install autonomously from the registry.
202
+ const serverResult = await requestServerInstall(catalogEntry.sourceRegistry, id, picked.version);
217
203
  if (!serverResult.ok) {
218
- let message = (_c = serverResult.error) !== null && _c !== void 0 ? _c : 'Server update failed';
204
+ let message = (_a = serverResult.error) !== null && _a !== void 0 ? _a : 'Server update failed';
219
205
  if (serverResult.code === 'missing-shards' && serverResult.missing) {
220
206
  const ids = serverResult.missing.map((m) => m.id).join(', ');
221
207
  message = `missing required shard(s): ${ids}`;
222
208
  }
223
209
  throw new Error(message);
224
210
  }
225
- // 7. Install locally (overwrites IndexedDB + re-registers). Reuse the
226
- // already-loaded bundle so the ESM is not evaluated twice.
227
- const result = await installPackage(bundle, meta, { loaded });
228
- if (!result.success) {
229
- // Rollback: restore old bundle and metadata.
230
- if (oldBundle) {
231
- try {
232
- await savePackage(id, oldBundle, oldRecord);
233
- }
234
- catch (rollbackErr) {
235
- console.warn(`[sh3-store] Rollback failed for "${id}":`, rollbackErr instanceof Error ? rollbackErr.message : rollbackErr);
211
+ // 8. Install locally from the already-fetched archive.
212
+ if (clientJs) {
213
+ const result = await installPackage(clientJs, meta, { loaded });
214
+ if (!result.success) {
215
+ if (oldBundle) {
216
+ try {
217
+ await savePackage(id, oldBundle, oldRecord);
218
+ }
219
+ catch (rollbackErr) {
220
+ console.warn(`[sh3-store] Rollback failed for "${id}":`, rollbackErr instanceof Error ? rollbackErr.message : rollbackErr);
221
+ }
236
222
  }
223
+ throw new Error((_b = result.error) !== null && _b !== void 0 ? _b : 'Local install failed during update');
237
224
  }
238
- throw new Error((_d = result.error) !== null && _d !== void 0 ? _d : 'Local install failed during update');
239
225
  }
240
226
  await refreshInstalled();
241
227
  }
@@ -5,9 +5,10 @@
5
5
  * Auto-prefixed to sh3-store:install, sh3-store:uninstall, sh3-store:appinfo.
6
6
  */
7
7
  import { storeContext } from './storeShard.svelte';
8
- import { fetchBundle, fetchServerBundle, buildPackageMeta } from '../../registry/client';
8
+ import { fetchArchive, buildPackageMeta } from '../../registry/client';
9
+ import { readManifestFromArchive, readFileFromArchive } from '../../registry/archive';
10
+ import { requestServerInstall } from '../../env/client';
9
11
  import { installPackage } from '../../registry/installer';
10
- import { serverInstallPackage } from '../../env/client';
11
12
  function findInCatalog(id) {
12
13
  return storeContext.state.ephemeral.catalog.find((p) => p.entry.id === id);
13
14
  }
@@ -19,7 +20,7 @@ export const installVerb = {
19
20
  summary: 'Install a package by id from the catalog.',
20
21
  programmatic: true,
21
22
  async run(ctx, args) {
22
- var _a, _b, _c;
23
+ var _a;
23
24
  const id = args[0];
24
25
  if (!id) {
25
26
  ctx.scrollback.push({
@@ -50,68 +51,36 @@ export const installVerb = {
50
51
  });
51
52
  return;
52
53
  }
53
- ctx.scrollback.push({
54
- kind: 'status',
55
- text: `installing ${id} v${pkg.latest.version}...`,
56
- level: 'info',
57
- ts: Date.now(),
58
- });
54
+ ctx.scrollback.push({ kind: 'status', text: `installing ${id} v${pkg.latest.version}...`, level: 'info', ts: Date.now() });
59
55
  try {
60
- const bundle = await fetchBundle(pkg.latest, pkg.sourceRegistry);
61
- const meta = buildPackageMeta(pkg, pkg.latest);
62
- let serverBundle;
63
- if (pkg.latest.serverBundleUrl) {
64
- serverBundle = await fetchServerBundle(pkg.latest, pkg.sourceRegistry);
65
- }
66
- const manifest = {
67
- id: meta.id,
68
- type: meta.type,
69
- label: pkg.entry.label,
70
- version: meta.version,
71
- contractVersion: meta.contractVersion,
72
- sourceRegistry: meta.sourceRegistry,
73
- installedAt: new Date().toISOString(),
74
- requiredShards: (_b = (_a = pkg.latest.requires) === null || _a === void 0 ? void 0 : _a.map((r) => r.id)) !== null && _b !== void 0 ? _b : [],
75
- };
76
- const serverResult = await serverInstallPackage(manifest, bundle, serverBundle);
56
+ // 1. Download archive for permission modal (future) and client local install
57
+ const archiveBytes = await fetchArchive(pkg.latest, pkg.sourceRegistry);
58
+ // 2. Extract manifest (permission modal reads from it)
59
+ readManifestFromArchive(archiveBytes);
60
+ // 3. Tell server to install autonomously
61
+ const serverResult = await requestServerInstall(pkg.sourceRegistry, id, pkg.latest.version);
77
62
  if (!serverResult.ok) {
78
- let text = `install failed: ${(_c = serverResult.error) !== null && _c !== void 0 ? _c : 'server error'}`;
63
+ let text = `install failed: ${(_a = serverResult.error) !== null && _a !== void 0 ? _a : 'server error'}`;
79
64
  if (serverResult.code === 'missing-shards' && serverResult.missing) {
80
- const ids = serverResult.missing.map((m) => m.id).join(', ');
81
- text = `install failed: missing required shard(s): ${ids}`;
65
+ text = `install failed: missing required shard(s): ${serverResult.missing.map((m) => m.id).join(', ')}`;
82
66
  }
83
- ctx.scrollback.push({
84
- kind: 'status',
85
- text,
86
- level: 'error',
87
- ts: Date.now(),
88
- });
67
+ ctx.scrollback.push({ kind: 'status', text, level: 'error', ts: Date.now() });
89
68
  return;
90
69
  }
91
- const result = await installPackage(bundle, meta);
92
- if (!result.success) {
93
- ctx.scrollback.push({
94
- kind: 'status',
95
- text: `server ok but local hot-load failed: ${result.error}`,
96
- level: 'warn',
97
- ts: Date.now(),
98
- });
70
+ // 4. Extract client.js from already-fetched archive and install locally
71
+ const clientJs = readFileFromArchive(archiveBytes, 'client.js');
72
+ if (clientJs) {
73
+ const meta = buildPackageMeta(pkg, pkg.latest);
74
+ const result = await installPackage(clientJs, meta);
75
+ if (!result.success) {
76
+ ctx.scrollback.push({ kind: 'status', text: `server ok but local hot-load failed: ${result.error}`, level: 'warn', ts: Date.now() });
77
+ }
99
78
  }
100
79
  await storeContext.refreshInstalled();
101
- ctx.scrollback.push({
102
- kind: 'status',
103
- text: `installed ${id} v${pkg.latest.version}`,
104
- level: 'info',
105
- ts: Date.now(),
106
- });
80
+ ctx.scrollback.push({ kind: 'status', text: `installed ${id} v${pkg.latest.version}`, level: 'info', ts: Date.now() });
107
81
  }
108
82
  catch (err) {
109
- ctx.scrollback.push({
110
- kind: 'status',
111
- text: `install failed: ${err instanceof Error ? err.message : String(err)}`,
112
- level: 'error',
113
- ts: Date.now(),
114
- });
83
+ ctx.scrollback.push({ kind: 'status', text: `install failed: ${err instanceof Error ? err.message : String(err)}`, level: 'error', ts: Date.now() });
115
84
  }
116
85
  },
117
86
  };
package/dist/build.d.ts CHANGED
@@ -82,4 +82,9 @@ export declare function composeArtifactVersion(pkgVersion: string, suffix: strin
82
82
  * Exported for testing; used internally by sh3Artifact.
83
83
  */
84
84
  export declare function extractRequiredShardsFromBundle(bundleSource: string): string[];
85
+ /**
86
+ * Collect all shard ids present in a bundle by scanning every `views: [` block.
87
+ * Exported for testing.
88
+ */
89
+ export declare function extractBundledShardIds(bundleSource: string): Set<string>;
85
90
  export declare function sh3Artifact(options?: Sh3ArtifactOptions): Plugin;
package/dist/build.js CHANGED
@@ -19,6 +19,7 @@
19
19
  import { readFileSync, writeFileSync, unlinkSync, readdirSync, copyFileSync, existsSync } from 'node:fs';
20
20
  import { execSync } from 'node:child_process';
21
21
  import { join } from 'node:path';
22
+ import { createArchive } from './registry/archive.js';
22
23
  /**
23
24
  * Vite plugin that inlines extracted CSS into the JS bundle.
24
25
  *
@@ -158,6 +159,50 @@ export function extractRequiredShardsFromBundle(bundleSource) {
158
159
  ids.push(m[1]);
159
160
  return ids;
160
161
  }
162
+ /**
163
+ * Collect all shard ids present in a bundle by scanning every `views: [` block.
164
+ * Exported for testing.
165
+ */
166
+ export function extractBundledShardIds(bundleSource) {
167
+ const ids = new Set();
168
+ const viewsRe = /\bviews\s*:\s*\[/g;
169
+ let m;
170
+ while ((m = viewsRe.exec(bundleSource)) !== null) {
171
+ // Walk back to the enclosing `{`.
172
+ let depth = 0;
173
+ let blockStart = m.index;
174
+ for (let i = m.index - 1; i >= 0; i--) {
175
+ if (bundleSource[i] === '}')
176
+ depth++;
177
+ if (bundleSource[i] === '{') {
178
+ if (depth === 0) {
179
+ blockStart = i;
180
+ break;
181
+ }
182
+ depth--;
183
+ }
184
+ }
185
+ // Walk forward to the matching `}`.
186
+ depth = 0;
187
+ let blockEnd = bundleSource.length;
188
+ for (let i = blockStart; i < bundleSource.length; i++) {
189
+ if (bundleSource[i] === '{')
190
+ depth++;
191
+ if (bundleSource[i] === '}') {
192
+ depth--;
193
+ if (depth === 0) {
194
+ blockEnd = i + 1;
195
+ break;
196
+ }
197
+ }
198
+ }
199
+ const block = bundleSource.slice(blockStart, blockEnd);
200
+ const idMatch = block.match(/\bid\s*:\s*["']([^"']+)["']/);
201
+ if (idMatch)
202
+ ids.add(idMatch[1]);
203
+ }
204
+ return ids;
205
+ }
161
206
  export function sh3Artifact(options = {}) {
162
207
  let outDir = '';
163
208
  let entryFileName = '';
@@ -264,7 +309,13 @@ export function sh3Artifact(options = {}) {
264
309
  }
265
310
  // App first, then Shard.
266
311
  const extracted = (_a = extractFromBlock(/\brequiredShards\s*:\s*\[/)) !== null && _a !== void 0 ? _a : extractFromBlock(/\bviews\s*:\s*\[/);
267
- const { id, label, requiredShards } = extracted;
312
+ const { id, label } = extracted;
313
+ let { requiredShards } = extracted;
314
+ // Strip any shard ids that are bundled in this artifact from requiredShards.
315
+ const bundledShardIds = extractBundledShardIds(source);
316
+ if (bundledShardIds.size > 0) {
317
+ requiredShards = requiredShards.filter(s => !bundledShardIds.has(s));
318
+ }
268
319
  // --- Optional server bundle ---
269
320
  let hasServer = false;
270
321
  if (options.serverEntry && existsSync(options.serverEntry)) {
@@ -311,14 +362,19 @@ export function sh3Artifact(options = {}) {
311
362
  if (!finalAuthor) {
312
363
  throw new Error('[sh3-artifact] Missing "author". Add it to package.json or pass it via sh3Artifact({ manifest: { author } }).');
313
364
  }
314
- const manifest = Object.assign(Object.assign(Object.assign(Object.assign({ id: id || 'unknown', type, label: label || id || 'unknown', version: artifactVersion, contractVersion: 1, client: 'client.js' }, (hasServer ? { server: 'server.js' } : {})), { description: finalDescription, author: finalAuthor }), ((type === 'app' || type === 'combo') ? { requiredShards } : {})), overrides);
315
- writeFileSync(join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
316
- // --- Log summary ---
317
- const files = ['manifest.json', 'client.js'];
365
+ const manifest = Object.assign(Object.assign(Object.assign(Object.assign({ id: id || 'unknown', type, label: label || id || 'unknown', version: artifactVersion, contractVersion: 1 }, (hasServer ? { server: 'server.js' } : {})), { description: finalDescription, author: finalAuthor }), ((type === 'app' || type === 'combo') ? { requiredShards } : {})), overrides);
366
+ // Read the emitted JS files as bytes for the archive
367
+ const clientBytes = readFileSync(join(outDir, 'client.js'));
368
+ const serverBytes = hasServer ? readFileSync(join(outDir, 'server.js')) : undefined;
369
+ // Create the .sh3pkg archive
370
+ const archive = createArchive(Object.assign({ manifest, client: new Uint8Array(clientBytes.buffer, clientBytes.byteOffset, clientBytes.byteLength) }, (serverBytes ? { server: new Uint8Array(serverBytes.buffer, serverBytes.byteOffset, serverBytes.byteLength) } : {})));
371
+ const archiveName = `${manifest.id}-${manifest.version}.sh3pkg`;
372
+ writeFileSync(join(outDir, archiveName), archive);
373
+ // Remove the loose files — the archive is the deliverable
374
+ unlinkSync(join(outDir, 'client.js'));
318
375
  if (hasServer)
319
- files.push('server.js');
320
- console.log(`[sh3-artifact] ${manifest.id}@${manifest.version} (${manifest.type}) → ${outDir}`);
321
- console.log(`[sh3-artifact] files: ${files.join(', ')}`);
376
+ unlinkSync(join(outDir, 'server.js'));
377
+ console.log(`[sh3-artifact] ${manifest.id}@${manifest.version} (${manifest.type}) → ${outDir}/${archiveName}`);
322
378
  },
323
379
  };
324
380
  }
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { composeArtifactVersion, extractRequiredShardsFromBundle } from './build';
2
+ import { composeArtifactVersion, extractRequiredShardsFromBundle, extractBundledShardIds } from './build';
3
3
  describe('composeArtifactVersion', () => {
4
4
  it('returns pkgVersion unchanged when suffix is undefined', () => {
5
5
  expect(composeArtifactVersion('1.2.3', undefined)).toBe('1.2.3');
@@ -55,3 +55,32 @@ describe('extractRequiredShardsFromBundle', () => {
55
55
  expect(extractRequiredShardsFromBundle(src)).toEqual(['guml.core', 'other-shard']);
56
56
  });
57
57
  });
58
+ describe('extractBundledShardIds', () => {
59
+ it('returns empty set when no shard blocks are present', () => {
60
+ const src = `const App = { id: "my-app", requiredShards: ["sh3-file-explorer"] };`;
61
+ expect(extractBundledShardIds(src)).toEqual(new Set());
62
+ });
63
+ it('extracts a single bundled shard id', () => {
64
+ const src = `const Shard = { id: "sh3-file-explorer", views: [] };`;
65
+ expect(extractBundledShardIds(src)).toEqual(new Set(['sh3-file-explorer']));
66
+ });
67
+ it('extracts multiple bundled shard ids', () => {
68
+ const src = `
69
+ const A = { id: "shard-a", views: [] };
70
+ const B = { id: "shard-b", views: [] };
71
+ `;
72
+ expect(extractBundledShardIds(src)).toEqual(new Set(['shard-a', 'shard-b']));
73
+ });
74
+ it('bundled shard ids are filtered from requiredShards', () => {
75
+ // Simulate a combo bundle: app requires shard-a and external-dep,
76
+ // but shard-a is present in the same bundle.
77
+ const src = `
78
+ const App = { id: "my-app", requiredShards: ["shard-a", "external-dep"] };
79
+ const Shard = { id: "shard-a", views: [] };
80
+ `;
81
+ const bundled = extractBundledShardIds(src);
82
+ const required = extractRequiredShardsFromBundle(src);
83
+ const filtered = required.filter(s => !bundled.has(s));
84
+ expect(filtered).toEqual(['external-dep']);
85
+ });
86
+ });
@@ -27,18 +27,14 @@ export interface ServerInstallResult {
27
27
  }>;
28
28
  }
29
29
  /**
30
- * Install a package on the server via multipart upload.
31
- * The client has already fetched and integrity-verified the client bundle
32
- * (and the server bundle, if present and serverIntegrity was declared).
30
+ * Request the server to install a package autonomously.
31
+ * The server fetches and validates the archive from the registry itself.
33
32
  *
34
- * @param manifest - Package manifest metadata to persist server-side.
35
- * @param clientBundle - Verified client ESM bundle bytes.
36
- * @param serverBundle - Optional verified server ESM bundle bytes. When
37
- * provided, the server writes it to `server.js` and hot-mounts the
38
- * shard's routes. If the mount fails, the entire install is rolled
39
- * back server-side.
33
+ * @param registryUrl - URL of the registry.json that lists the package.
34
+ * @param packageId - The package id to install.
35
+ * @param version - The exact version string to install.
40
36
  */
41
- export declare function serverInstallPackage(manifest: Record<string, unknown>, clientBundle: ArrayBuffer, serverBundle?: ArrayBuffer): Promise<ServerInstallResult>;
37
+ export declare function requestServerInstall(registryUrl: string, packageId: string, version: string): Promise<ServerInstallResult>;
42
38
  /**
43
39
  * Uninstall a package from the server.
44
40
  */
@@ -45,35 +45,25 @@ export async function putEnvState(shardId, state) {
45
45
  }
46
46
  }
47
47
  /**
48
- * Install a package on the server via multipart upload.
49
- * The client has already fetched and integrity-verified the client bundle
50
- * (and the server bundle, if present and serverIntegrity was declared).
48
+ * Request the server to install a package autonomously.
49
+ * The server fetches and validates the archive from the registry itself.
51
50
  *
52
- * @param manifest - Package manifest metadata to persist server-side.
53
- * @param clientBundle - Verified client ESM bundle bytes.
54
- * @param serverBundle - Optional verified server ESM bundle bytes. When
55
- * provided, the server writes it to `server.js` and hot-mounts the
56
- * shard's routes. If the mount fails, the entire install is rolled
57
- * back server-side.
51
+ * @param registryUrl - URL of the registry.json that lists the package.
52
+ * @param packageId - The package id to install.
53
+ * @param version - The exact version string to install.
58
54
  */
59
- export async function serverInstallPackage(manifest, clientBundle, serverBundle) {
55
+ export async function requestServerInstall(registryUrl, packageId, version) {
60
56
  var _a;
61
57
  if (!isAdmin())
62
58
  throw new Error('Cannot install: not elevated to admin');
63
59
  const auth = getAuthHeader();
64
- const form = new FormData();
65
- form.append('manifest', new Blob([JSON.stringify(manifest)], { type: 'application/json' }), 'manifest.json');
66
- form.append('client', new Blob([clientBundle], { type: 'application/javascript' }), 'client.js');
67
- if (serverBundle !== undefined) {
68
- form.append('server', new Blob([serverBundle], { type: 'application/javascript' }), 'server.js');
69
- }
70
- const headers = {};
60
+ const headers = { 'Content-Type': 'application/json' };
71
61
  if (auth)
72
62
  headers['Authorization'] = auth;
73
63
  const res = await apiFetch(`${getEnvServerUrl()}/api/packages/install`, {
74
64
  method: 'POST',
75
65
  headers,
76
- body: form,
66
+ body: JSON.stringify({ registryUrl, packageId, version }),
77
67
  credentials: 'omit',
78
68
  });
79
69
  if (!res.ok) {
@@ -84,9 +74,9 @@ export async function serverInstallPackage(manifest, clientBundle, serverBundle)
84
74
  catch ( /* non-JSON */_b) { /* non-JSON */ }
85
75
  return {
86
76
  ok: false,
87
- error: typeof body.error === 'string' ? body.error : `HTTP ${res.status}`,
88
- code: typeof body.code === 'string' ? body.code : undefined,
89
- missing: Array.isArray(body.missing) ? body.missing : undefined,
77
+ error: typeof body['error'] === 'string' ? body['error'] : `HTTP ${res.status}`,
78
+ code: typeof body['code'] === 'string' ? body['code'] : undefined,
79
+ missing: Array.isArray(body['missing']) ? body['missing'] : undefined,
90
80
  };
91
81
  }
92
82
  const body = await res.json();
@@ -1,2 +1,3 @@
1
1
  export type { EnvState } from './types';
2
- export { __setEnvServerUrl, getEnvServerUrl, fetchEnvState, putEnvState, serverInstallPackage, serverUninstallPackage, fetchServerPackages, } from './client';
2
+ export { __setEnvServerUrl, getEnvServerUrl, fetchEnvState, putEnvState, requestServerInstall, serverUninstallPackage, fetchServerPackages, } from './client';
3
+ export type { ServerInstallResult } from './client';
package/dist/env/index.js CHANGED
@@ -1 +1 @@
1
- export { __setEnvServerUrl, getEnvServerUrl, fetchEnvState, putEnvState, serverInstallPackage, serverUninstallPackage, fetchServerPackages, } from './client';
1
+ export { __setEnvServerUrl, getEnvServerUrl, fetchEnvState, putEnvState, requestServerInstall, serverUninstallPackage, fetchServerPackages, } from './client';