@astrale-os/cli 1.0.0-beta.19 → 1.0.0-beta.20

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.
@@ -0,0 +1,150 @@
1
+ import type { ChildrenChannel, ExternalOpenRequest, Shell } from '@astrale-os/shell'
2
+
3
+ import { capabilitiesMiddleware, createIntentPipeline, createIntentRouter } from '@astrale-os/shell'
4
+ import { describe, expect, mock, test } from 'bun:test'
5
+
6
+ import type { ExternalOpenIntentMessage } from '../view/external-open-intent'
7
+
8
+ import {
9
+ installExternalOpenIntentHandler,
10
+ openExternalBrowserWindow,
11
+ } from '../view/external-open-intent'
12
+ import { viewHostCapabilities } from '../view/host-capabilities'
13
+
14
+ describe('View external navigation host effect', () => {
15
+ test('the real Shell pipeline opens only an exact granted origin and replies to the physical child', async () => {
16
+ const sent: Array<{ windowId: string; message: unknown }> = []
17
+ const children = {
18
+ send: (windowId: string, value: unknown) => sent.push({ windowId, message: value }),
19
+ has: () => true,
20
+ on: () => () => undefined,
21
+ onClose: () => () => undefined,
22
+ } as ChildrenChannel
23
+ const capabilities = viewHostCapabilities(['https://connect.nango.dev'])
24
+ const pipeline = createIntentPipeline()
25
+ pipeline.use(
26
+ capabilitiesMiddleware({
27
+ lookup: (sender) => (sender === 'physical-child' ? capabilities : undefined),
28
+ }),
29
+ )
30
+ const router = createIntentRouter({
31
+ selfWindowId: 'root',
32
+ pipeline,
33
+ parent: null,
34
+ children,
35
+ })
36
+ const open = mock((_request: ExternalOpenRequest) => true)
37
+ installExternalOpenIntentHandler(
38
+ {
39
+ children,
40
+ onIntent: router.onLocal.bind(router),
41
+ } as unknown as Shell,
42
+ { open },
43
+ )
44
+
45
+ await router.handleInbound(
46
+ 'child',
47
+ message('https://connect.nango.dev/session', 'forged-sibling'),
48
+ 'physical-child',
49
+ )
50
+
51
+ expect(open).toHaveBeenCalledTimes(1)
52
+ expect(sent).toEqual([
53
+ {
54
+ windowId: 'physical-child',
55
+ message: expect.objectContaining({
56
+ envelope: expect.objectContaining({
57
+ payload: { correlationId: 'request-1', result: { outcome: 'opened' } },
58
+ }),
59
+ }),
60
+ },
61
+ ])
62
+
63
+ for (const denied of [
64
+ 'http://connect.nango.dev/session',
65
+ 'https://sub.connect.nango.dev/session',
66
+ 'https://connect.nango.dev:444/session',
67
+ 'https://evil.example/session',
68
+ ]) {
69
+ await router.handleInbound('child', message(denied), 'physical-child')
70
+ }
71
+ await router.handleInbound('child', message('https://connect.nango.dev/session'), 'ungranted')
72
+ expect(open).toHaveBeenCalledTimes(1)
73
+ expect(sent).toHaveLength(1)
74
+ })
75
+
76
+ test('reports a popup block through the admitted pipeline', async () => {
77
+ const sent = mock((_windowId: string, _message: unknown) => undefined)
78
+ const children = {
79
+ send: sent,
80
+ has: () => true,
81
+ on: () => () => undefined,
82
+ onClose: () => () => undefined,
83
+ } as ChildrenChannel
84
+ const pipeline = createIntentPipeline()
85
+ pipeline.use(
86
+ capabilitiesMiddleware({
87
+ lookup: () => viewHostCapabilities(['https://connect.nango.dev']),
88
+ }),
89
+ )
90
+ const router = createIntentRouter({ selfWindowId: 'root', pipeline, parent: null, children })
91
+ installExternalOpenIntentHandler(
92
+ { children, onIntent: router.onLocal.bind(router) } as unknown as Shell,
93
+ { open: () => false },
94
+ )
95
+
96
+ await router.handleInbound('child', message('https://connect.nango.dev/session'), 'child-1')
97
+
98
+ expect(sent).toHaveBeenCalledWith(
99
+ 'child-1',
100
+ expect.objectContaining({
101
+ envelope: expect.objectContaining({
102
+ payload: { correlationId: 'request-1', result: { outcome: 'blocked' } },
103
+ }),
104
+ }),
105
+ )
106
+ })
107
+
108
+ test('isolates a fresh inert context before navigating and closes it when isolation fails', () => {
109
+ const replace = mock((_url: string) => undefined)
110
+ const opened = { opener: {}, location: { replace }, close: mock(() => undefined) }
111
+ const open = mock(() => opened)
112
+
113
+ expect(
114
+ openExternalBrowserWindow({ open } as never, {
115
+ url: 'https://connect.nango.dev/session',
116
+ mode: 'popup',
117
+ }),
118
+ ).toBe(true)
119
+ expect(open).toHaveBeenCalledWith('', '_blank', 'popup,width=720,height=760')
120
+ expect(opened.opener).toBeNull()
121
+ expect(replace).toHaveBeenCalledWith('https://connect.nango.dev/session')
122
+
123
+ const close = mock(() => undefined)
124
+ const unsafe = Object.defineProperty({ close, location: { replace: mock() } }, 'opener', {
125
+ set: () => {
126
+ throw new Error('opener isolation refused')
127
+ },
128
+ })
129
+ expect(
130
+ openExternalBrowserWindow({ open: () => unsafe } as never, {
131
+ url: 'https://connect.nango.dev/session',
132
+ mode: 'tab',
133
+ }),
134
+ ).toBe(false)
135
+ expect(close).toHaveBeenCalledTimes(1)
136
+ })
137
+ })
138
+
139
+ function message(url: string, sender = 'child-1'): ExternalOpenIntentMessage {
140
+ return {
141
+ type: 'intent',
142
+ version: 1,
143
+ envelope: {
144
+ name: 'browser.openExternal',
145
+ payload: { url, mode: 'popup' },
146
+ sender: { windowId: sender },
147
+ correlationId: 'request-1',
148
+ },
149
+ }
150
+ }
@@ -0,0 +1,27 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import { admitExternalOpenOrigins } from '../view/external-open-origins'
4
+
5
+ describe('View external navigation grants', () => {
6
+ test('defaults to no authority and canonicalizes exact HTTPS origins', () => {
7
+ expect(admitExternalOpenOrigins(undefined)).toEqual([])
8
+ expect(
9
+ admitExternalOpenOrigins([
10
+ 'https://connect.nango.dev',
11
+ 'https://connect.nango.dev/',
12
+ 'https://connect.composio.dev:443',
13
+ ]),
14
+ ).toEqual(['https://connect.nango.dev', 'https://connect.composio.dev'])
15
+ })
16
+
17
+ test.each([
18
+ 'http://connect.nango.dev',
19
+ 'https://user@connect.nango.dev',
20
+ 'https://connect.nango.dev/path',
21
+ 'https://connect.nango.dev?session=secret',
22
+ 'https://*.example.com',
23
+ 'not-a-url',
24
+ ])('rejects a non-origin grant: %s', (candidate) => {
25
+ expect(() => admitExternalOpenOrigins([candidate])).toThrow('exact HTTPS origin')
26
+ })
27
+ })
@@ -60,12 +60,20 @@ describe('view session server credentials', () => {
60
60
  issuer: 'https://kernel.test',
61
61
  direct: true,
62
62
  },
63
+ externalOrigins: ['https://connect.nango.dev'],
63
64
  idleMs: 60_000,
64
65
  } satisfies ViewServeConfig
65
66
  const server = startViewServer(config)
66
67
  await once(server, 'listening')
67
68
 
68
69
  try {
70
+ const configResponse = await fetch(`http://127.0.0.1:${port}/s/${nonce}/config.json`)
71
+ expect(configResponse.status).toBe(200)
72
+ expect(await configResponse.json()).toMatchObject({
73
+ sessionId: 'v-plain',
74
+ externalOrigins: ['https://connect.nango.dev'],
75
+ })
76
+
69
77
  const response = await fetch(`http://127.0.0.1:${port}/s/${nonce}/token`, {
70
78
  method: 'POST',
71
79
  })
@@ -69,6 +69,7 @@ describe('view session private state', () => {
69
69
  issuer: 'https://kernel.test',
70
70
  direct: true,
71
71
  },
72
+ externalOrigins: [],
72
73
  idleMs: 60_000,
73
74
  } satisfies ViewServeConfig
74
75
 
package/src/lib/update.ts CHANGED
@@ -231,9 +231,40 @@ async function realpathIfExists(path: string): Promise<string | undefined> {
231
231
  export async function writeInstallMetadata(
232
232
  meta: InstallMetadata,
233
233
  path = INSTALL_PATH,
234
+ filesystem: Pick<CohortFilesystem, 'mkdir' | 'rename' | 'rm'> &
235
+ Pick<typeof import('node:fs/promises'), 'writeFile'> = { mkdir, rename, rm, writeFile },
234
236
  ): Promise<void> {
235
- await mkdir(dirname(path), { recursive: true })
236
- await writeFile(path, JSON.stringify(meta, null, 2) + '\n')
237
+ const staged = `${path}.next`
238
+ const previous = `${path}.previous`
239
+ await filesystem.mkdir(dirname(path), { recursive: true })
240
+ await filesystem.rm(staged, { force: true })
241
+ await filesystem.rm(previous, { force: true })
242
+ await filesystem.writeFile(staged, JSON.stringify(meta, null, 2) + '\n')
243
+
244
+ let backedUp = false
245
+ try {
246
+ try {
247
+ await filesystem.rename(path, previous)
248
+ backedUp = true
249
+ } catch (error) {
250
+ if (!isMissingFile(error)) throw error
251
+ }
252
+ await filesystem.rename(staged, path)
253
+ await filesystem.rm(previous, { force: true })
254
+ } catch (error) {
255
+ const rollback: unknown[] = []
256
+ if (backedUp) {
257
+ await filesystem.rename(previous, path).catch((failure) => rollback.push(failure))
258
+ }
259
+ await filesystem.rm(staged, { force: true }).catch((failure) => rollback.push(failure))
260
+ if (rollback.length > 0) {
261
+ throw new AggregateError(
262
+ [error, ...rollback],
263
+ 'Install metadata update and rollback both failed.',
264
+ )
265
+ }
266
+ throw error
267
+ }
237
268
  }
238
269
 
239
270
  export function releaseBase(
@@ -258,7 +289,127 @@ export function shouldUpdate(currentVersion: string, manifestVersion: string): b
258
289
  return currentVersion !== manifestVersion
259
290
  }
260
291
 
261
- export async function updateAstrale(req: UpdateRequest): Promise<UpdateResult> {
292
+ interface CohortFilesystem {
293
+ readonly chmod: typeof chmod
294
+ readonly copyFile: typeof copyFile
295
+ readonly mkdir: typeof mkdir
296
+ readonly rename: typeof rename
297
+ readonly rm: typeof rm
298
+ }
299
+
300
+ const defaultCohortFilesystem: CohortFilesystem = { chmod, copyFile, mkdir, rename, rm }
301
+
302
+ export interface StandaloneCohortReplacement {
303
+ readonly finalize: () => Promise<void>
304
+ readonly rollback: () => Promise<void>
305
+ }
306
+
307
+ /** Replace the standalone executable and its Viewer as one rollback-safe cohort. */
308
+ export async function replaceStandaloneCohort(
309
+ installedBinary: string,
310
+ nextBinary: string,
311
+ nextViewerDist: string,
312
+ filesystem: Partial<CohortFilesystem> = {},
313
+ ): Promise<StandaloneCohortReplacement> {
314
+ const fs = { ...defaultCohortFilesystem, ...filesystem }
315
+ const binDirectory = dirname(installedBinary)
316
+ const previousBinary = `${installedBinary}.previous`
317
+ const stagedBinary = `${installedBinary}.next`
318
+ const viewer = join(binDirectory, 'viewer')
319
+ const previousViewer = join(binDirectory, 'viewer.previous')
320
+ const stagedViewer = join(binDirectory, 'viewer.next')
321
+ const stagedViewerDist = join(stagedViewer, 'dist')
322
+
323
+ try {
324
+ await fs.rm(stagedViewer, { recursive: true, force: true })
325
+ await fs.mkdir(stagedViewerDist, { recursive: true })
326
+ await fs.copyFile(join(nextViewerDist, 'main.js'), join(stagedViewerDist, 'main.js'))
327
+ await fs.copyFile(join(nextViewerDist, 'index.html'), join(stagedViewerDist, 'index.html'))
328
+ await fs.copyFile(installedBinary, previousBinary)
329
+ await fs.copyFile(nextBinary, stagedBinary)
330
+ await fs.chmod(stagedBinary, 0o755)
331
+ } catch (error) {
332
+ await fs.rm(stagedViewer, { recursive: true, force: true }).catch(() => undefined)
333
+ await fs.rm(stagedBinary, { force: true }).catch(() => undefined)
334
+ throw error
335
+ }
336
+
337
+ await fs.rm(previousViewer, { recursive: true, force: true })
338
+ let viewerBackedUp = false
339
+ let viewerCommitted = false
340
+ let binaryCommitted = false
341
+ try {
342
+ try {
343
+ await fs.rename(viewer, previousViewer)
344
+ viewerBackedUp = true
345
+ } catch (error) {
346
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
347
+ }
348
+ await fs.rename(stagedViewer, viewer)
349
+ viewerCommitted = true
350
+ await fs.rename(stagedBinary, installedBinary)
351
+ binaryCommitted = true
352
+ } catch (error) {
353
+ const rollback: unknown[] = []
354
+ if (binaryCommitted) {
355
+ await fs.copyFile(previousBinary, installedBinary).catch((failure) => rollback.push(failure))
356
+ await fs.chmod(installedBinary, 0o755).catch((failure) => rollback.push(failure))
357
+ }
358
+ if (viewerCommitted) {
359
+ await fs
360
+ .rm(viewer, { recursive: true, force: true })
361
+ .catch((failure) => rollback.push(failure))
362
+ }
363
+ if (viewerBackedUp) {
364
+ await fs.rename(previousViewer, viewer).catch((failure) => rollback.push(failure))
365
+ }
366
+ if (rollback.length > 0) {
367
+ throw new AggregateError([error, ...rollback], 'Standalone update and rollback both failed.')
368
+ }
369
+ throw error
370
+ } finally {
371
+ await fs.rm(stagedViewer, { recursive: true, force: true }).catch(() => undefined)
372
+ await fs.rm(stagedBinary, { force: true }).catch(() => undefined)
373
+ }
374
+
375
+ let settled = false
376
+ return Object.freeze({
377
+ finalize: async () => {
378
+ if (settled) return
379
+ settled = true
380
+ await fs.rm(previousViewer, { recursive: true, force: true }).catch(() => undefined)
381
+ },
382
+ rollback: async () => {
383
+ if (settled) return
384
+ settled = true
385
+ const rollback: unknown[] = []
386
+ await fs.copyFile(previousBinary, installedBinary).catch((failure) => rollback.push(failure))
387
+ await fs.chmod(installedBinary, 0o755).catch((failure) => rollback.push(failure))
388
+ await fs
389
+ .rm(viewer, { recursive: true, force: true })
390
+ .catch((failure) => rollback.push(failure))
391
+ if (viewerBackedUp) {
392
+ await fs.rename(previousViewer, viewer).catch((failure) => rollback.push(failure))
393
+ }
394
+ if (rollback.length > 0) {
395
+ throw new AggregateError(rollback, 'Standalone update rollback failed.')
396
+ }
397
+ },
398
+ })
399
+ }
400
+
401
+ interface UpdateDependencies {
402
+ readonly replaceStandaloneCohort: typeof replaceStandaloneCohort
403
+ readonly writeInstallMetadata: typeof writeInstallMetadata
404
+ }
405
+
406
+ const defaultUpdateDependencies = Object.freeze({ replaceStandaloneCohort, writeInstallMetadata })
407
+
408
+ export async function updateAstrale(
409
+ req: UpdateRequest,
410
+ dependencies: Partial<UpdateDependencies> = {},
411
+ ): Promise<UpdateResult> {
412
+ const update = { ...defaultUpdateDependencies, ...dependencies }
262
413
  const execution = req.execution ?? detectUpdateExecution()
263
414
  if (execution.kind === 'package-managed') {
264
415
  return {
@@ -320,25 +471,33 @@ export async function updateAstrale(req: UpdateRequest): Promise<UpdateResult> {
320
471
 
321
472
  await extractTarGz(archive, tmp)
322
473
  const nextBin = join(tmp, 'astrale')
474
+ const nextViewer = join(tmp, 'viewer', 'dist')
323
475
  await chmod(nextBin, 0o755)
324
476
  await smokeVersion(nextBin, manifest.binaryVersion ?? manifest.version)
325
477
 
326
- const previous = `${meta.bin}.previous`
327
- const staged = `${meta.bin}.next`
328
- await copyFile(meta.bin, previous).catch(() => undefined)
329
- await copyFile(nextBin, staged)
330
- await chmod(staged, 0o755)
331
- await rename(staged, meta.bin)
332
-
333
- await writeInstallMetadata(
334
- {
335
- ...meta,
336
- channel: manifest.channel,
337
- version: manifest.version,
338
- installedAt: new Date().toISOString(),
339
- },
340
- req.installPath,
341
- )
478
+ const replacement = await update.replaceStandaloneCohort(meta.bin, nextBin, nextViewer)
479
+ try {
480
+ await update.writeInstallMetadata(
481
+ {
482
+ ...meta,
483
+ channel: manifest.channel,
484
+ version: manifest.version,
485
+ installedAt: new Date().toISOString(),
486
+ },
487
+ req.installPath,
488
+ )
489
+ } catch (error) {
490
+ try {
491
+ await replacement.rollback()
492
+ } catch (rollbackError) {
493
+ throw new AggregateError(
494
+ [error, rollbackError],
495
+ 'Standalone update metadata commit and cohort rollback both failed.',
496
+ )
497
+ }
498
+ throw error
499
+ }
500
+ await replacement.finalize()
342
501
 
343
502
  return {
344
503
  status: 'updated',
@@ -8,7 +8,11 @@ import { fileURLToPath } from 'node:url'
8
8
  * (`<pkg>/viewer/dist`). The module URL is authoritative because npm global
9
9
  * installs expose the CLI through a bin symlink outside the package root.
10
10
  */
11
- export function viewerDistDir(moduleUrl = import.meta.url, entry = process.argv[1] ?? '.'): string {
11
+ export function viewerDistDir(
12
+ moduleUrl = import.meta.url,
13
+ entry = process.argv[1] ?? '.',
14
+ executable = process.execPath,
15
+ ): string {
12
16
  const override = process.env.ASTRALE_VIEWER_DIR
13
17
  if (override) return override
14
18
 
@@ -16,7 +20,12 @@ export function viewerDistDir(moduleUrl = import.meta.url, entry = process.argv[
16
20
  const published = join(moduleDirectory, '..', 'viewer', 'dist')
17
21
  const source = join(moduleDirectory, '..', '..', '..', 'viewer', 'dist')
18
22
  const legacy = join(dirname(entry), '..', 'viewer', 'dist')
19
- const complete = [published, source, legacy].find(hasViewerBundle)
23
+ const standalone = entry.startsWith('/$bunfs/')
24
+ ? join(dirname(executable), 'viewer', 'dist')
25
+ : undefined
26
+ const complete = [standalone, published, source, legacy].find(
27
+ (candidate): candidate is string => candidate !== undefined && hasViewerBundle(candidate),
28
+ )
20
29
  if (complete) return complete
21
30
 
22
31
  // A source checkout may intentionally omit generated dist assets. Keep the
@@ -0,0 +1,41 @@
1
+ import type { ExternalOpenRequest, IntentMessage, Shell } from '@astrale-os/shell'
2
+
3
+ import { replyToIntent } from '@astrale-os/shell'
4
+
5
+ export interface ExternalOpenIntentHost {
6
+ open(request: ExternalOpenRequest): boolean
7
+ }
8
+
9
+ /** Register the root host's browser-owned external navigation effect. */
10
+ export function installExternalOpenIntentHandler(
11
+ shell: Shell,
12
+ host: ExternalOpenIntentHost,
13
+ ): () => void {
14
+ return shell.onIntent('browser.openExternal', (message) => {
15
+ const opened = host.open(message.envelope.payload)
16
+ replyToIntent(shell.children, message.envelope.sender.windowId, message, {
17
+ outcome: opened ? 'opened' : 'blocked',
18
+ })
19
+ })
20
+ }
21
+
22
+ /** Open an external document without retaining a cross-origin opener capability. */
23
+ export function openExternalBrowserWindow(
24
+ browser: Pick<Window, 'open'>,
25
+ request: ExternalOpenRequest,
26
+ ): boolean {
27
+ const popup = request.mode === 'popup'
28
+ const opened = browser.open('', '_blank', popup ? 'popup,width=720,height=760' : undefined)
29
+ if (opened === null) return false
30
+ try {
31
+ opened.opener = null
32
+ if (opened.opener !== null) throw new Error('Browser retained the opener capability.')
33
+ opened.location.replace(request.url)
34
+ } catch {
35
+ opened.close()
36
+ return false
37
+ }
38
+ return true
39
+ }
40
+
41
+ export type ExternalOpenIntentMessage = IntentMessage<'browser.openExternal'>
@@ -0,0 +1,37 @@
1
+ import { AstraleError } from '../../errors.js'
2
+
3
+ /** Admit exact HTTPS origins explicitly granted by the CLI operator. */
4
+ export function admitExternalOpenOrigins(input: readonly string[] | undefined): readonly string[] {
5
+ if (input === undefined) return Object.freeze([])
6
+ const admitted = new Set<string>()
7
+ for (const candidate of input) {
8
+ let url: URL
9
+ try {
10
+ url = new URL(candidate)
11
+ } catch (cause) {
12
+ throw invalidExternalOrigin(candidate, cause)
13
+ }
14
+ if (
15
+ url.protocol !== 'https:' ||
16
+ url.hostname.includes('*') ||
17
+ url.username !== '' ||
18
+ url.password !== '' ||
19
+ url.pathname !== '/' ||
20
+ url.search !== '' ||
21
+ url.hash !== ''
22
+ ) {
23
+ throw invalidExternalOrigin(candidate)
24
+ }
25
+ admitted.add(url.origin)
26
+ }
27
+ return Object.freeze([...admitted])
28
+ }
29
+
30
+ function invalidExternalOrigin(candidate: string, cause?: unknown): AstraleError {
31
+ return new AstraleError(
32
+ 'INVALID_EXTERNAL_ORIGIN',
33
+ `External navigation grant "${candidate}" is not an exact HTTPS origin.`,
34
+ 'Pass an origin such as https://connect.example.com with no credentials, path, query, or fragment.',
35
+ cause === undefined ? undefined : { cause },
36
+ )
37
+ }
@@ -0,0 +1,17 @@
1
+ import type { HostCapabilities } from '@astrale-os/shell'
2
+
3
+ import { admitHostCapabilities } from '@astrale-os/shell'
4
+
5
+ /** Build the exact host grant handed to a CLI-hosted View session. */
6
+ export function viewHostCapabilities(externalOrigins: readonly string[]): HostCapabilities {
7
+ return admitHostCapabilities({
8
+ version: 1,
9
+ navigation: {
10
+ openView: {},
11
+ ...(externalOrigins.length === 0 ? {} : { external: { origins: externalOrigins } }),
12
+ },
13
+ actions: {},
14
+ browser: {},
15
+ access: {},
16
+ })
17
+ }
@@ -101,6 +101,7 @@ export function startViewServer(config: ViewServeConfig): Server {
101
101
  identity: session.identity ?? null,
102
102
  instance: session.instance ?? null,
103
103
  sessionId: session.id,
104
+ externalOrigins: config.externalOrigins,
104
105
  })
105
106
  return
106
107
  }
@@ -41,6 +41,8 @@ export type ViewServeConfig = {
41
41
  * handles CORS and self-signed local CAs.
42
42
  */
43
43
  proxy: { kernelUrl: string; issuer: string; caFile?: string; direct: boolean }
44
+ /** Exact HTTPS origins the operator consented to open from this View session. */
45
+ externalOrigins: readonly string[]
44
46
  idleMs: number
45
47
  }
46
48
 
@@ -195,7 +195,7 @@ describe('program composition', () => {
195
195
  'whoami',
196
196
  ])
197
197
  expect(createHash('sha256').update(JSON.stringify(surface)).digest('hex')).toBe(
198
- '958b54c4eeee1bab77efeac734bd11fe5a0b103aae69788de7db5418e5652f64',
198
+ '51546dd7ea5cbf25efbd41f0cd09ceb480abad5be489fc16743500a18e3a02c6',
199
199
  )
200
200
  })
201
201
 
@@ -21,8 +21,8 @@
21
21
  "typecheck": "tsgo --noEmit"
22
22
  },
23
23
  "dependencies": {
24
- "@astrale-os/sdk": "0.5.0-beta.49",
25
- "@astrale-os/shell": "0.4.2-beta.3",
24
+ "@astrale-os/sdk": "0.5.0-beta.50",
25
+ "@astrale-os/shell": "0.4.2-beta.5",
26
26
  "@dagrejs/dagre": "^3.0.0",
27
27
  "@radix-ui/react-collapsible": "^1.1.0",
28
28
  "@radix-ui/react-dialog": "^1.1.6",