@astrale-os/cli 1.0.0-beta.4 → 1.0.0-beta.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.
@@ -322,6 +322,7 @@ export declare function fetchOidcMetadata(issuer: string): Promise<OidcMetadata>
322
322
  export declare function workosAuthKitMetadata(apiHost?: string, clientId?: string): OidcMetadata;
323
323
  export declare function builtinIdpConfig(name: string, clientIdOverride?: string, env?: NodeJS.ProcessEnv): IdpConfig | null;
324
324
  export declare function workosClientIdFromEnv(env?: NodeJS.ProcessEnv): string | undefined;
325
+ export declare function legacyBuiltinWorkosReplacement(existing: IdpConfig, clientIdOverride?: string, env?: NodeJS.ProcessEnv): IdpConfig | undefined;
325
326
  export declare function postForm(url: string, params: URLSearchParams): Promise<TokenResponse>;
326
327
  export declare function postJson(url: string, bodyInput: Record<string, string>): Promise<TokenResponse>;
327
328
  export declare function tokenExpiresAt(token: TokenResponse): string | undefined;
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { AstraleError } from '../errors';
2
3
  export declare const DEFAULT_UPDATE_CHANNEL = "beta";
3
4
  export declare const InstallMetadataSchema: z.ZodObject<{
4
5
  method: z.ZodLiteral<"script">;
@@ -73,8 +74,26 @@ export type UpdateRequest = {
73
74
  currentVersion: string;
74
75
  platform?: Platform;
75
76
  installPath?: string;
77
+ execution?: UpdateExecution;
76
78
  };
79
+ export type UpdateExecution = {
80
+ kind: 'standalone';
81
+ executable: string;
82
+ } | {
83
+ kind: 'package-managed';
84
+ executable: string;
85
+ };
86
+ declare const admittedScriptInstall: unique symbol;
87
+ export type AdmittedScriptInstall = Readonly<{
88
+ metadata: InstallMetadata;
89
+ executable: string;
90
+ [admittedScriptInstall]: true;
91
+ }>;
77
92
  export type UpdateResult = {
93
+ status: 'managed';
94
+ currentVersion: string;
95
+ executable: string;
96
+ } | {
78
97
  status: 'up-to-date';
79
98
  currentVersion: string;
80
99
  latestVersion: string;
@@ -93,9 +112,20 @@ export type UpdateResult = {
93
112
  };
94
113
  export declare function detectPlatform(): Platform;
95
114
  export declare function platformKey(platform: Platform): string;
115
+ export declare function classifyUpdateExecution(input: {
116
+ bunVersion?: string;
117
+ executable: string;
118
+ entry?: string;
119
+ }): UpdateExecution;
120
+ export declare function detectUpdateExecution(): UpdateExecution;
121
+ export declare function packageManagedUpdateError(executable: string): AstraleError;
96
122
  export declare function readInstallMetadata(path?: string): Promise<InstallMetadata>;
123
+ export declare function admitScriptInstall(meta: InstallMetadata, execution: Extract<UpdateExecution, {
124
+ kind: 'standalone';
125
+ }>): Promise<AdmittedScriptInstall>;
97
126
  export declare function writeInstallMetadata(meta: InstallMetadata, path?: string): Promise<void>;
98
127
  export declare function releaseBase(meta: InstallMetadata, req: Pick<UpdateRequest, 'channel' | 'version'>): string;
99
128
  export declare function fetchManifest(base: string): Promise<UpdateManifest>;
100
129
  export declare function shouldUpdate(currentVersion: string, manifestVersion: string): boolean;
101
130
  export declare function updateAstrale(req: UpdateRequest): Promise<UpdateResult>;
131
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -62,11 +62,14 @@
62
62
  "zod": "^3.25.0"
63
63
  },
64
64
  "devDependencies": {
65
- "@astrale-os/kernel-client": ">=0.6.0-beta.4 <1.0.0",
66
- "@astrale-os/kernel-protocol": ">=0.5.0-beta.3 <1.0.0",
65
+ "@astrale-os/kernel-client": ">=0.6.0-beta.8 <1.0.0",
66
+ "@astrale-os/kernel-core": ">=0.9.0-beta.7 <1.0.0",
67
+ "@astrale-os/kernel-dsl": ">=0.2.0-beta.5 <1.0.0",
68
+ "@astrale-os/kernel-protocol": ">=0.5.0-beta.7 <1.0.0",
69
+ "@astrale-os/kernel-server": ">=0.5.0-beta.8 <1.0.0",
67
70
  "@astrale-os/ox": ">=0.1.3 <1.0.0",
68
- "@astrale-os/sdk": ">=0.5.0-beta.9 <1.0.0",
69
- "@astrale-os/shell": ">=0.3.8-beta.2 <1.0.0",
71
+ "@astrale-os/sdk": ">=0.5.0-beta.14 <1.0.0",
72
+ "@astrale-os/shell": ">=0.3.8-beta.3 <1.0.0",
70
73
  "@astrale/commitlint-config": "npm:@jsr/astrale__commitlint-config@~2.0.1",
71
74
  "@commitlint/cli": "~20.3.1",
72
75
  "@commitlint/config-conventional": "~20.3.1",
@@ -20,7 +20,7 @@ const deployment = createDeployment({
20
20
  })
21
21
 
22
22
  test('binds the installed Admin Domain instead of the source Kernel publication', async () => {
23
- const installed = mock(async () => ({
23
+ const installation = mock(async () => ({
24
24
  state: 'ready' as const,
25
25
  target: 'sha256:admin-target' as const,
26
26
  source: { kind: 'remote' as const, publication: deployment.publication },
@@ -28,11 +28,11 @@ test('binds the installed Admin Domain instead of the source Kernel publication'
28
28
  readiness: 'sha256:admin-readiness' as const,
29
29
  capabilities: { requested: [], materialized: [] },
30
30
  }))
31
- const session = { installed } as unknown as ClientSession
31
+ const session = { installation } as unknown as ClientSession
32
32
 
33
33
  const binding = await bindAdmin(session)
34
34
 
35
- expect(installed).toHaveBeenCalledWith('admin.astrale.ai')
35
+ expect(installation).toHaveBeenCalledWith('admin.astrale.ai')
36
36
  expect(binding.$.origin).toBe('admin.astrale.ai')
37
37
  expect(String(binding.$.publication?.identity.issuer)).toBe('https://admin.beta.astrale.ai')
38
38
  })
@@ -1,7 +1,7 @@
1
- import type { DomainBinding } from '@astrale-os/kernel-client/domain'
2
1
  import type { ClientSession } from '@astrale-os/kernel-client/session'
2
+ import type { DomainBinding } from '@astrale-os/shell'
3
3
 
4
- import { bind } from '@astrale-os/kernel-client/domain'
4
+ import { bindDomain } from '@astrale-os/shell'
5
5
 
6
6
  const ADMIN_ORIGIN = 'admin.astrale.ai'
7
7
 
@@ -9,8 +9,8 @@ export type AdminBinding = DomainBinding
9
9
 
10
10
  /** Bind the exact Admin revision installed on this source Kernel. */
11
11
  export async function bindAdmin(session: ClientSession): Promise<AdminBinding> {
12
- const installed = await session.installed(ADMIN_ORIGIN)
13
- const binding = bind(session, installed)
12
+ const installed = await session.installation(ADMIN_ORIGIN)
13
+ const binding = await bindDomain(session, installed.bundle.root)
14
14
  if (binding.$.publication?.origin !== ADMIN_ORIGIN || binding.$.origin !== ADMIN_ORIGIN) {
15
15
  throw new TypeError('Configured Admin target does not serve the Admin Domain.')
16
16
  }
@@ -1,5 +1,5 @@
1
- import type { DomainBinding } from '@astrale-os/kernel-client/domain'
2
1
  import type { ClientSession } from '@astrale-os/kernel-client/session'
2
+ import type { DomainBinding } from '@astrale-os/shell'
3
3
 
4
4
  import type { AdminGraphApi } from '../../graph/.spec/api.js'
5
5
 
@@ -1,5 +1,5 @@
1
- import type { DomainBinding } from '@astrale-os/kernel-client/domain'
2
1
  import type { ClientSession } from '@astrale-os/kernel-client/session'
2
+ import type { DomainBinding } from '@astrale-os/shell'
3
3
 
4
4
  import type { AdminGraphApi } from '../../graph/.spec/api.js'
5
5
 
@@ -35,9 +35,11 @@ describe('CLI update staleness', () => {
35
35
  const result = await cliStale(
36
36
  { channel: 'canary' },
37
37
  {
38
- update: async () => {
39
- throw new Error('package managed')
40
- },
38
+ update: async () => ({
39
+ status: 'managed',
40
+ currentVersion: '1.0.0-beta.0',
41
+ executable: '/opt/homebrew/bin/node',
42
+ }),
41
43
  fetchPackageVersion: async ({ channel }) => {
42
44
  expect(channel).toBe('canary')
43
45
  return '1.0.0-canary.0'
@@ -52,6 +54,26 @@ describe('CLI update staleness', () => {
52
54
  })
53
55
  })
54
56
 
57
+ test('does not misclassify a script update failure as package-managed', async () => {
58
+ const fetchPackageVersion = mock(async () => '9.9.9')
59
+ const result = await cliStale(
60
+ {},
61
+ {
62
+ update: async () => {
63
+ throw new Error('release endpoint unavailable')
64
+ },
65
+ fetchPackageVersion,
66
+ },
67
+ )
68
+
69
+ expect(result).toMatchObject({
70
+ stale: false,
71
+ managed: false,
72
+ error: 'release endpoint unavailable',
73
+ })
74
+ expect(fetchPackageVersion).not.toHaveBeenCalled()
75
+ })
76
+
55
77
  test('maps the default to beta and the stable channel to npm latest', async () => {
56
78
  const originalFetch = globalThis.fetch
57
79
  const fetchMock = mock(async (input: RequestInfo | URL) => {
@@ -12,7 +12,7 @@ import {
12
12
  type SdkOutdated,
13
13
  } from '../lib/sdk-deps'
14
14
  import { ASTRALE_CLI_SKILL, detectSkill, installSkills, SKILL_INSTALL_HINT } from '../lib/skills'
15
- import { DEFAULT_UPDATE_CHANNEL, updateAstrale } from '../lib/update'
15
+ import { DEFAULT_UPDATE_CHANNEL, packageManagedUpdateError, updateAstrale } from '../lib/update'
16
16
 
17
17
  type UpdateOpts = RawOutputOpts & {
18
18
  check?: boolean
@@ -104,14 +104,22 @@ async function refreshSdkDeps(check: boolean, assumeYes = false): Promise<boolea
104
104
  /**
105
105
  * Read-only staleness report for tooling — `astrale update --check --json`.
106
106
  * domain-studio polls this on load to drive its "update available" badge. It is
107
- * unified and NON-THROWING: the CLI axis catches the package-managed / no-metadata
108
- * case (reported as `managed`, never stale), and the SDK axis is already
109
- * best-effort. Skills are intentionally absent they ride along with `astrale
110
- * update`, so a stale CLI or SDK is the only signal worth surfacing.
107
+ * unified and NON-THROWING: an explicit package-managed result uses npm release
108
+ * identity, while script-install failures remain script failures in `error`
109
+ * instead of being recategorized. The SDK axis is already best-effort. Skills are
110
+ * intentionally absent — they ride along with `astrale update`, so a stale CLI or
111
+ * SDK is the only signal worth surfacing.
111
112
  */
112
113
  export type StaleReport = {
113
114
  stale: boolean
114
- cli: { stale: boolean; managed: boolean; current?: string; latest?: string; channel?: string }
115
+ cli: {
116
+ stale: boolean
117
+ managed: boolean
118
+ current?: string
119
+ latest?: string
120
+ channel?: string
121
+ error?: string
122
+ }
115
123
  sdk: { stale: boolean; inProject: boolean; outdated: SdkOutdated[] }
116
124
  }
117
125
 
@@ -138,6 +146,15 @@ export async function cliStale(
138
146
  version: target.version,
139
147
  currentVersion: running,
140
148
  })
149
+ if (r.status === 'managed') {
150
+ const latest = await dependencies.fetchPackageVersion(target).catch(() => undefined)
151
+ return {
152
+ stale: latest !== undefined && latest !== running,
153
+ managed: true,
154
+ current: running,
155
+ ...(latest === undefined ? {} : { latest, channel: 'npm' }),
156
+ }
157
+ }
141
158
  if (r.status === 'updated') {
142
159
  return {
143
160
  stale: false,
@@ -154,13 +171,12 @@ export async function cliStale(
154
171
  latest: r.latestVersion,
155
172
  channel: r.channel,
156
173
  }
157
- } catch {
158
- const latest = await dependencies.fetchPackageVersion(target).catch(() => undefined)
174
+ } catch (error) {
159
175
  return {
160
- stale: latest !== undefined && latest !== running,
161
- managed: true,
176
+ stale: false,
177
+ managed: false,
162
178
  current: running,
163
- ...(latest === undefined ? {} : { latest, channel: 'npm' }),
179
+ error: error instanceof Error ? error.message : String(error),
164
180
  }
165
181
  }
166
182
  }
@@ -294,6 +310,10 @@ Examples:
294
310
  log.success(`Updated astrale ${result.previousVersion} -> ${result.currentVersion}`)
295
311
  log.dim(` channel: ${result.channel}`)
296
312
  log.dim(` binary: ${result.bin}`)
313
+ } else if (result?.status === 'managed') {
314
+ const error = packageManagedUpdateError(result.executable)
315
+ if (!opts.yes) throw error
316
+ log.warn(`CLI self-update skipped: ${error.message}`)
297
317
  }
298
318
 
299
319
  // Axis B — agent skills. Keep an existing install current with the CLI.
@@ -9,6 +9,7 @@ import {
9
9
  IdpSessionSchema,
10
10
  isSessionExpired,
11
11
  issuerFromToken,
12
+ legacyBuiltinWorkosReplacement,
12
13
  normalizeTokenResponse,
13
14
  OAuthTokenError,
14
15
  OidcMetadataSchema,
@@ -84,6 +85,30 @@ describe('IdP schemas and token helpers', () => {
84
85
  )
85
86
  })
86
87
 
88
+ test('replaces only the legacy persisted built-in WorkOS client', () => {
89
+ const legacy = builtinIdpConfig('workos', 'client_01KC29HEGD7B40TV2C4QZ436BG', {})!
90
+ const replacement = legacyBuiltinWorkosReplacement(legacy, undefined, {})
91
+
92
+ expect(replacement?.client.client_id).toBe('client_01KC29HET5F3QAQ8GNTPZ7F320')
93
+ expect(replacement?.metadata.jwks_uri).toBe(
94
+ 'https://api.workos.com/sso/jwks/client_01KC29HET5F3QAQ8GNTPZ7F320',
95
+ )
96
+ expect(
97
+ legacyBuiltinWorkosReplacement(
98
+ { ...legacy, entry: { ...legacy.entry, builtIn: false } },
99
+ undefined,
100
+ {},
101
+ ),
102
+ ).toBeUndefined()
103
+ expect(
104
+ legacyBuiltinWorkosReplacement(
105
+ { ...legacy, client: { ...legacy.client, client_id: 'client_custom' } },
106
+ undefined,
107
+ {},
108
+ ),
109
+ ).toBeUndefined()
110
+ })
111
+
87
112
  test('stores client secret env names, not secret material', () => {
88
113
  const client = IdpClientConfigSchema.parse({
89
114
  client_id: 'client_123',
@@ -1,16 +1,20 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { chmod, mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'
2
+ import { chmod, mkdir, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'
3
3
  import { tmpdir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
6
  import {
7
+ admitScriptInstall,
8
+ classifyUpdateExecution,
7
9
  DEFAULT_UPDATE_CHANNEL,
8
10
  InstallMetadataSchema,
11
+ readInstallMetadata,
9
12
  releaseBase,
10
13
  shouldUpdate,
11
14
  updateAstrale,
12
15
  writeInstallMetadata,
13
16
  type InstallMetadata,
17
+ type UpdateExecution,
14
18
  } from '../update'
15
19
 
16
20
  async function makeFakeRelease(
@@ -65,7 +69,7 @@ async function makeFakeRelease(
65
69
  async function makeInstall(
66
70
  root: string,
67
71
  version: string,
68
- ): Promise<{ meta: InstallMetadata; path: string }> {
72
+ ): Promise<{ meta: InstallMetadata; path: string; execution: UpdateExecution }> {
69
73
  const bin = join(root, 'bin', 'astrale')
70
74
  await mkdir(join(root, 'bin'), { recursive: true })
71
75
  await writeFile(
@@ -82,10 +86,33 @@ async function makeInstall(
82
86
  bin,
83
87
  }
84
88
  await writeInstallMetadata(meta, path)
85
- return { meta, path }
89
+ return { meta, path, execution: { kind: 'standalone', executable: bin } }
86
90
  }
87
91
 
88
92
  describe('update helpers', () => {
93
+ test('classifies only a Bun-compiled executable as standalone', () => {
94
+ expect(
95
+ classifyUpdateExecution({
96
+ bunVersion: '1.3.14',
97
+ executable: '/tmp/astrale',
98
+ entry: '/$bunfs/root/astrale',
99
+ }),
100
+ ).toEqual({ kind: 'standalone', executable: '/tmp/astrale' })
101
+ expect(
102
+ classifyUpdateExecution({
103
+ bunVersion: '1.3.14',
104
+ executable: '/opt/homebrew/bin/bun',
105
+ entry: '/tmp/astrale.ts',
106
+ }),
107
+ ).toEqual({ kind: 'package-managed', executable: '/opt/homebrew/bin/bun' })
108
+ expect(
109
+ classifyUpdateExecution({
110
+ executable: '/opt/homebrew/bin/node',
111
+ entry: '/tmp/astrale.js',
112
+ }),
113
+ ).toEqual({ kind: 'package-managed', executable: '/opt/homebrew/bin/node' })
114
+ })
115
+
89
116
  test('defaults missing install metadata to the beta channel', () => {
90
117
  expect(DEFAULT_UPDATE_CHANNEL).toBe('beta')
91
118
  expect(
@@ -118,10 +145,70 @@ describe('update helpers', () => {
118
145
  })
119
146
  })
120
147
 
148
+ describe('script install admission', () => {
149
+ test('rejects malformed JSON with the stable metadata error', async () => {
150
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
151
+ const path = join(root, 'install.json')
152
+ await writeFile(path, '{')
153
+
154
+ await expect(readInstallMetadata(path)).rejects.toMatchObject({
155
+ code: 'UPDATE_BAD_INSTALL_METADATA',
156
+ })
157
+ })
158
+
159
+ test('admits a symlink only when it resolves to the recorded binary', async () => {
160
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
161
+ const { meta } = await makeInstall(root, '1.0.0')
162
+ const alias = join(root, 'astrale-alias')
163
+ await symlink(meta.bin, alias)
164
+
165
+ const admitted = await admitScriptInstall(meta, {
166
+ kind: 'standalone',
167
+ executable: alias,
168
+ })
169
+
170
+ expect(admitted.metadata).toEqual(meta)
171
+ })
172
+
173
+ test('rejects a standalone binary that does not own the recorded target', async () => {
174
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
175
+ const { meta } = await makeInstall(root, '1.0.0')
176
+ const other = join(root, 'other-astrale')
177
+ await writeFile(other, '#!/bin/sh\n')
178
+
179
+ await expect(
180
+ admitScriptInstall(meta, { kind: 'standalone', executable: other }),
181
+ ).rejects.toMatchObject({ code: 'UPDATE_INSTALL_MISMATCH' })
182
+ })
183
+ })
184
+
121
185
  describe('updateAstrale', () => {
186
+ test('a package-managed process never consults or mutates a coexisting script install', async () => {
187
+ const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
188
+ const { path, meta } = await makeInstall(root, '1.0.0')
189
+ const before = await readFile(meta.bin, 'utf8')
190
+ process.env.ASTRALE_UPDATE_BASE = 'file:///definitely-not-a-release'
191
+ try {
192
+ const result = await updateAstrale({
193
+ currentVersion: '2.0.0',
194
+ installPath: path,
195
+ execution: { kind: 'package-managed', executable: '/opt/homebrew/bin/node' },
196
+ })
197
+
198
+ expect(result).toEqual({
199
+ status: 'managed',
200
+ currentVersion: '2.0.0',
201
+ executable: '/opt/homebrew/bin/node',
202
+ })
203
+ expect(await readFile(meta.bin, 'utf8')).toBe(before)
204
+ } finally {
205
+ delete process.env.ASTRALE_UPDATE_BASE
206
+ }
207
+ })
208
+
122
209
  test('check compares the installed release identity from metadata', async () => {
123
210
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
124
- const { path } = await makeInstall(root, 'main-abc123')
211
+ const { path, execution } = await makeInstall(root, 'main-abc123')
125
212
  const release = await makeFakeRelease(root, 'main-abc123', { binaryVersion: '1.0.0' })
126
213
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
127
214
  try {
@@ -130,6 +217,7 @@ describe('updateAstrale', () => {
130
217
  currentVersion: '1.0.0',
131
218
  platform: { os: 'darwin', arch: 'arm64' },
132
219
  installPath: path,
220
+ execution,
133
221
  })
134
222
 
135
223
  expect(result).toMatchObject({
@@ -144,7 +232,7 @@ describe('updateAstrale', () => {
144
232
 
145
233
  test('check reports available update without replacing the binary', async () => {
146
234
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
147
- const { path, meta } = await makeInstall(root, '1.0.0')
235
+ const { path, meta, execution } = await makeInstall(root, '1.0.0')
148
236
  const release = await makeFakeRelease(root, '1.1.0')
149
237
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
150
238
  try {
@@ -153,6 +241,7 @@ describe('updateAstrale', () => {
153
241
  currentVersion: '1.0.0',
154
242
  platform: { os: 'darwin', arch: 'arm64' },
155
243
  installPath: path,
244
+ execution,
156
245
  })
157
246
 
158
247
  expect(result).toMatchObject({
@@ -168,7 +257,7 @@ describe('updateAstrale', () => {
168
257
 
169
258
  test('supports legacy string asset manifests', async () => {
170
259
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
171
- const { path, meta } = await makeInstall(root, '1.0.0')
260
+ const { path, meta, execution } = await makeInstall(root, '1.0.0')
172
261
  const release = await makeFakeRelease(root, '1.1.0', { legacyManifest: true })
173
262
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
174
263
  try {
@@ -177,6 +266,7 @@ describe('updateAstrale', () => {
177
266
  currentVersion: '1.0.0',
178
267
  platform: { os: 'darwin', arch: 'arm64' },
179
268
  installPath: path,
269
+ execution,
180
270
  })
181
271
 
182
272
  expect(check).toMatchObject({
@@ -188,6 +278,7 @@ describe('updateAstrale', () => {
188
278
  currentVersion: '1.0.0',
189
279
  platform: { os: 'darwin', arch: 'arm64' },
190
280
  installPath: path,
281
+ execution,
191
282
  })
192
283
 
193
284
  expect(result).toMatchObject({
@@ -204,7 +295,7 @@ describe('updateAstrale', () => {
204
295
 
205
296
  test('updates the binary and install metadata', async () => {
206
297
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
207
- const { path, meta } = await makeInstall(root, '1.0.0')
298
+ const { path, meta, execution } = await makeInstall(root, '1.0.0')
208
299
  const release = await makeFakeRelease(root, '1.1.0')
209
300
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
210
301
  try {
@@ -212,6 +303,7 @@ describe('updateAstrale', () => {
212
303
  currentVersion: '1.0.0',
213
304
  platform: { os: 'darwin', arch: 'arm64' },
214
305
  installPath: path,
306
+ execution,
215
307
  })
216
308
 
217
309
  expect(result).toMatchObject({
@@ -231,7 +323,7 @@ describe('updateAstrale', () => {
231
323
 
232
324
  test('updates canary-style releases whose binary reports the package version', async () => {
233
325
  const root = await mkdtemp(join(tmpdir(), 'astrale-update-test-'))
234
- const { path, meta } = await makeInstall(root, 'main-old123')
326
+ const { path, meta, execution } = await makeInstall(root, 'main-old123')
235
327
  const release = await makeFakeRelease(root, 'main-new456', { binaryVersion: '1.0.0' })
236
328
  process.env.ASTRALE_UPDATE_BASE = `file://${release}`
237
329
  try {
@@ -239,6 +331,7 @@ describe('updateAstrale', () => {
239
331
  currentVersion: '1.0.0',
240
332
  platform: { os: 'darwin', arch: 'arm64' },
241
333
  installPath: path,
334
+ execution,
242
335
  })
243
336
 
244
337
  expect(result).toMatchObject({
@@ -23,7 +23,6 @@ const profile: ResolvedView = {
23
23
  kind: 'definition',
24
24
  definitions: [{ origin: 'shell.test', kind: 'class', name: 'Person' }],
25
25
  },
26
- auth: 'required',
27
26
  },
28
27
  href: 'https://shell.test/profile',
29
28
  handshake: 'shell',
@@ -41,7 +40,6 @@ const card: ResolvedView = {
41
40
  kind: 'definition',
42
41
  definitions: [{ origin: 'shell.test', kind: 'class', name: 'Person' }],
43
42
  },
44
- auth: 'public',
45
43
  },
46
44
  href: 'https://shell.test/card',
47
45
  handshake: 'none',
@@ -29,7 +29,7 @@ describe('view session server credentials', () => {
29
29
  target: target('/:example.test'),
30
30
  route: {
31
31
  key: 'example.test:view.public',
32
- declaration: { target: { kind: 'domain' }, auth: 'required' },
32
+ declaration: { target: { kind: 'domain' } },
33
33
  href: 'https://example.test/ui/public',
34
34
  handshake: 'none',
35
35
  issuer: issuer('https://example.test'),
@@ -58,7 +58,7 @@ describe('view session private state', () => {
58
58
  issuer: issuer('https://example.test'),
59
59
  etag: `sha256:${'a'.repeat(64)}`,
60
60
  revision: revision('b'),
61
- declaration: { target: { kind: 'domain' }, auth: 'required' },
61
+ declaration: { target: { kind: 'domain' } },
62
62
  },
63
63
  },
64
64
  createdAt: '2026-08-12T00:00:00.000Z',
package/src/lib/idp.ts CHANGED
@@ -215,6 +215,7 @@ export type IdpConfig = {
215
215
  export const BUILTIN_WORKOS_IDP_NAME = 'workos'
216
216
  const DEFAULT_WORKOS_API_HOST = 'https://api.workos.com'
217
217
  const DEFAULT_WORKOS_CLIENT_ID = 'client_01KC29HET5F3QAQ8GNTPZ7F320'
218
+ const LEGACY_WORKOS_CLIENT_ID = 'client_01KC29HEGD7B40TV2C4QZ436BG'
218
219
  const WORKOS_CLIENT_ID_ENV_NAMES = ['WORKOS_CLIENT_ID', 'VITE_WORKOS_CLIENT_ID'] as const
219
220
 
220
221
  export function idpDir(name: string): string {
@@ -281,7 +282,17 @@ export async function readIdpConfigOrBuiltin(
281
282
  validateName(name, 'IdP')
282
283
  const store = await readIdpStore()
283
284
  if (store.idps[name]) {
284
- return await readIdpConfig(name)
285
+ const existing = await readIdpConfig(name)
286
+ const replacement = opts.persist
287
+ ? legacyBuiltinWorkosReplacement(existing, opts.clientId)
288
+ : undefined
289
+ if (!replacement) return existing
290
+ return upsertIdpConfig({
291
+ name: replacement.name,
292
+ metadata: replacement.metadata,
293
+ client: replacement.client,
294
+ builtIn: true,
295
+ })
285
296
  }
286
297
 
287
298
  const builtin = builtinIdpConfig(name, opts.clientId)
@@ -421,6 +432,23 @@ export function workosClientIdFromEnv(env: NodeJS.ProcessEnv = process.env): str
421
432
  )
422
433
  }
423
434
 
435
+ export function legacyBuiltinWorkosReplacement(
436
+ existing: IdpConfig,
437
+ clientIdOverride?: string,
438
+ env: NodeJS.ProcessEnv = process.env,
439
+ ): IdpConfig | undefined {
440
+ if (
441
+ existing.name !== BUILTIN_WORKOS_IDP_NAME ||
442
+ !existing.entry.builtIn ||
443
+ existing.client.client_id !== LEGACY_WORKOS_CLIENT_ID
444
+ ) {
445
+ return undefined
446
+ }
447
+ const replacement = builtinIdpConfig(existing.name, clientIdOverride, env)
448
+ if (!replacement || replacement.client.client_id === LEGACY_WORKOS_CLIENT_ID) return undefined
449
+ return replacement
450
+ }
451
+
424
452
  async function fetchOAuthAuthorizationServerMetadata(
425
453
  issuer: string,
426
454
  ): Promise<Partial<OidcMetadata> | undefined> {