@astrale-os/cli 1.0.0-beta.24 → 1.0.0-beta.26

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/astrale.js CHANGED
@@ -2578,7 +2578,7 @@ var package_default;
2578
2578
  var init_package = __esm(() => {
2579
2579
  package_default = {
2580
2580
  name: "@astrale-os/cli",
2581
- version: "1.0.0-beta.24",
2581
+ version: "1.0.0-beta.26",
2582
2582
  description: "Astrale CLI — connect to existing Astrale kernels",
2583
2583
  keywords: [
2584
2584
  "astrale",
@@ -86741,8 +86741,18 @@ async function json4(fetcher, url3, label) {
86741
86741
  cause
86742
86742
  });
86743
86743
  }
86744
+ const body = await responseText(response2, label);
86745
+ try {
86746
+ return JSON.parse(body);
86747
+ } catch (cause) {
86748
+ throw new UiError("UI_REGISTRY_UNAVAILABLE", label + " returned malformed JSON.", undefined, {
86749
+ cause
86750
+ });
86751
+ }
86752
+ }
86753
+ async function responseText(response2, label) {
86744
86754
  if (!response2.ok) {
86745
- throw new UiError("UI_REGISTRY_UNAVAILABLE", label + " returned HTTP " + response2.status + ".");
86755
+ throw new UiError("UI_REGISTRY_UNAVAILABLE", label + " returned HTTP " + response2.status + ".", undefined, { cause: new HttpStatusError(response2.status) });
86746
86756
  }
86747
86757
  const declaredLength = Number(response2.headers.get("content-length"));
86748
86758
  if (Number.isFinite(declaredLength) && declaredLength > MAX_DOCUMENT_BYTES) {
@@ -86771,13 +86781,7 @@ async function json4(fetcher, url3, label) {
86771
86781
  body.set(chunk, offset);
86772
86782
  offset += chunk.byteLength;
86773
86783
  }
86774
- try {
86775
- return JSON.parse(new TextDecoder().decode(body));
86776
- } catch (cause) {
86777
- throw new UiError("UI_REGISTRY_UNAVAILABLE", label + " returned malformed JSON.", undefined, {
86778
- cause
86779
- });
86780
- }
86784
+ return new TextDecoder().decode(body);
86781
86785
  }
86782
86786
  async function resolveUiRelease(requested, fetcher = fetch) {
86783
86787
  const versionDocument = requested ? { version: requested.replace(/^v/u, "") } : await json4(fetcher, NPM_PACKAGE + "/beta", "npm UI release");
@@ -86789,13 +86793,45 @@ async function resolveUiRelease(requested, fetcher = fetch) {
86789
86793
  throw new UiError("UI_REGISTRY_UNAVAILABLE", "Invalid UI beta release version: " + version2);
86790
86794
  }
86791
86795
  const ref = "v" + version2;
86792
- const reference4 = await json4(fetcher, GITHUB_API + "/git/ref/tags/" + encodeURIComponent(ref), "UI ref " + ref);
86793
- const commit = reference4.object.type === "commit" ? reference4.object.sha : (await json4(fetcher, reference4.object.url, "annotated UI tag " + ref)).object.sha;
86796
+ const commit = await resolveReleaseCommit(ref, fetcher);
86794
86797
  if (!/^[0-9a-f]{40}$/u.test(commit)) {
86795
86798
  throw new UiError("UI_REGISTRY_UNAVAILABLE", "UI ref " + ref + " did not resolve to a commit.");
86796
86799
  }
86797
86800
  return readUiReleaseSnapshot({ version: version2, ref, commit }, fetcher);
86798
86801
  }
86802
+ async function resolveReleaseCommit(ref, fetcher) {
86803
+ try {
86804
+ const reference4 = await json4(fetcher, GITHUB_API + "/git/ref/tags/" + encodeURIComponent(ref), "UI ref " + ref);
86805
+ return reference4.object.type === "commit" ? reference4.object.sha : (await json4(fetcher, reference4.object.url, "annotated UI tag " + ref)).object.sha;
86806
+ } catch (cause) {
86807
+ const status = cause instanceof UiError && cause.cause instanceof HttpStatusError ? cause.cause.status : undefined;
86808
+ if (status !== 403 && status !== 429 && !(status !== undefined && status >= 500))
86809
+ throw cause;
86810
+ }
86811
+ const label = "UI release " + ref;
86812
+ let response2;
86813
+ try {
86814
+ response2 = await fetcher(GITHUB_WEB + "/releases/tag/" + encodeURIComponent(ref), {
86815
+ headers: { accept: "text/html" }
86816
+ });
86817
+ } catch (cause) {
86818
+ throw new UiError("UI_REGISTRY_UNAVAILABLE", "Unable to reach " + label + ".", undefined, {
86819
+ cause
86820
+ });
86821
+ }
86822
+ const html = await responseText(response2, label);
86823
+ const marker = 'href="/astrale-os/ui/tree/' + ref + '"';
86824
+ const markerOffset = html.lastIndexOf(marker);
86825
+ const releaseHeader = markerOffset < 0 ? "" : html.slice(markerOffset, markerOffset + 8192);
86826
+ const commits = new Set([
86827
+ ...releaseHeader.matchAll(/data-hovercard-type=["']commit["'][^>]+href=["']\/astrale-os\/ui\/commit\/([0-9a-f]{40})["']/gu)
86828
+ ].map((match) => match[1]));
86829
+ const commit = commits.size === 1 ? commits.values().next().value : undefined;
86830
+ if (!commit) {
86831
+ throw new UiError("UI_REGISTRY_UNAVAILABLE", "UI ref " + ref + " did not resolve to a commit.");
86832
+ }
86833
+ return commit;
86834
+ }
86799
86835
  async function readUiReleaseSnapshot(identity3, fetcher = fetch) {
86800
86836
  const [compatibility, registry2] = await Promise.all([
86801
86837
  json4(fetcher, RAW + "/" + identity3.commit + "/tooling/compatibility.json", "UI compatibility metadata"),
@@ -86907,9 +86943,16 @@ function isSafeRelative2(value3) {
86907
86943
  function registryItemUrl(release, itemName) {
86908
86944
  return RAW + "/" + release.commit + "/registry/public/r/" + encodeURIComponent(itemName) + ".json";
86909
86945
  }
86910
- var NPM_PACKAGE = "https://registry.npmjs.org/@astrale-os/ui", GITHUB_API = "https://api.github.com/repos/astrale-os/ui", RAW = "https://raw.githubusercontent.com/astrale-os/ui", MAX_DOCUMENT_BYTES = 1048576, MAX_REGISTRY_DOCUMENTS = 100;
86946
+ var NPM_PACKAGE = "https://registry.npmjs.org/@astrale-os/ui", GITHUB_API = "https://api.github.com/repos/astrale-os/ui", GITHUB_WEB = "https://github.com/astrale-os/ui", RAW = "https://raw.githubusercontent.com/astrale-os/ui", MAX_DOCUMENT_BYTES = 1048576, MAX_REGISTRY_DOCUMENTS = 100, HttpStatusError;
86911
86947
  var init_release = __esm(() => {
86912
86948
  init_model7();
86949
+ HttpStatusError = class HttpStatusError extends Error {
86950
+ status;
86951
+ constructor(status) {
86952
+ super("HTTP " + status);
86953
+ this.status = status;
86954
+ }
86955
+ };
86913
86956
  });
86914
86957
 
86915
86958
  // src/ui/runner.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "1.0.0-beta.24",
3
+ "version": "1.0.0-beta.26",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -330,6 +330,97 @@ describe('UI release and runner contracts', () => {
330
330
  )
331
331
  })
332
332
 
333
+ test('resolves the immutable release commit when the anonymous GitHub API is rate limited', async () => {
334
+ const seen: string[] = []
335
+ const fallback = mockFetch(seen)
336
+ const fetcher = (async (input: string | URL | Request, init?: RequestInit) => {
337
+ const url = String(input)
338
+ if (url.includes('/git/ref/tags/')) {
339
+ seen.push(url)
340
+ return Response.json({ message: 'API rate limit exceeded' }, { status: 403 })
341
+ }
342
+ if (url.includes('/releases/tag/')) {
343
+ seen.push(url)
344
+ return new Response(
345
+ `<a data-hovercard-type="commit" href="/astrale-os/ui/commit/${'b'.repeat(40)}">decoy</a>
346
+ <a href="/astrale-os/ui/tree/v0.3.0-beta.0">repository navigation</a>
347
+ ${'x'.repeat(35_000)}
348
+ <a href="/astrale-os/ui/tree/v0.3.0-beta.0">tag</a>
349
+ <a data-hovercard-type="commit" href="/astrale-os/ui/commit/${commit}">commit</a>`,
350
+ { headers: { 'content-type': 'text/html' } },
351
+ )
352
+ }
353
+ return fallback(input, init)
354
+ }) as typeof fetch
355
+
356
+ const release = await resolveUiRelease('0.3.0-beta.0', fetcher)
357
+
358
+ expect(release.commit).toBe(commit)
359
+ expect(seen).toContain('https://github.com/astrale-os/ui/releases/tag/v0.3.0-beta.0')
360
+ expect(seen).toContain(
361
+ 'https://raw.githubusercontent.com/astrale-os/ui/' + commit + '/tooling/compatibility.json',
362
+ )
363
+ })
364
+
365
+ test('rejects a release page without an exact Astrale UI commit target', async () => {
366
+ const fetcher = (async (input: string | URL | Request) => {
367
+ const url = String(input)
368
+ if (url.includes('/git/ref/tags/')) return new Response('limited', { status: 403 })
369
+ if (url.includes('/releases/tag/')) return new Response('<main>release unavailable</main>')
370
+ throw new Error('release snapshot must not be fetched')
371
+ }) as typeof fetch
372
+
373
+ await expect(resolveUiRelease('0.3.0-beta.0', fetcher)).rejects.toMatchObject({
374
+ code: 'UI_REGISTRY_UNAVAILABLE',
375
+ message: 'UI ref v0.3.0-beta.0 did not resolve to a commit.',
376
+ })
377
+ })
378
+
379
+ test('does not replace an authoritative missing tag with release-page HTML', async () => {
380
+ const seen: string[] = []
381
+ const fetcher = (async (input: string | URL | Request) => {
382
+ const url = String(input)
383
+ seen.push(url)
384
+ if (url.includes('/git/ref/tags/')) return new Response('missing', { status: 404 })
385
+ throw new Error('release page must not be fetched')
386
+ }) as typeof fetch
387
+
388
+ await expect(resolveUiRelease('0.3.0-beta.0', fetcher)).rejects.toMatchObject({
389
+ code: 'UI_REGISTRY_UNAVAILABLE',
390
+ message: 'UI ref v0.3.0-beta.0 returned HTTP 404.',
391
+ })
392
+ expect(seen).toEqual(['https://api.github.com/repos/astrale-os/ui/git/ref/tags/v0.3.0-beta.0'])
393
+ })
394
+
395
+ test('bounds declared and streamed release-page HTML before reading a snapshot', async () => {
396
+ for (const oversized of [
397
+ new Response('large', { headers: { 'content-length': '1048577' } }),
398
+ new Response(
399
+ new ReadableStream({
400
+ start(controller) {
401
+ controller.enqueue(new Uint8Array(1_048_577))
402
+ controller.close()
403
+ },
404
+ }),
405
+ ),
406
+ ]) {
407
+ const seen: string[] = []
408
+ const fetcher = (async (input: string | URL | Request) => {
409
+ const url = String(input)
410
+ seen.push(url)
411
+ if (url.includes('/git/ref/tags/')) return new Response('limited', { status: 403 })
412
+ if (url.includes('/releases/tag/')) return oversized
413
+ throw new Error('release snapshot must not be fetched')
414
+ }) as typeof fetch
415
+
416
+ await expect(resolveUiRelease('0.3.0-beta.0', fetcher)).rejects.toMatchObject({
417
+ code: 'UI_REGISTRY_UNAVAILABLE',
418
+ message: 'UI release v0.3.0-beta.0 exceeds the supported response size.',
419
+ })
420
+ expect(seen).toHaveLength(2)
421
+ }
422
+ })
423
+
333
424
  test('admits a release theme as one canonical consumer-owned CSS target', async () => {
334
425
  const release = await resolveUiRelease('0.3.0-beta.0', themeFetch())
335
426
  expect(release.registry.items).toEqual([
package/src/ui/release.ts CHANGED
@@ -2,11 +2,17 @@ import { UiError, type UiCompatibility, type UiRegistry, type UiRelease } from '
2
2
 
3
3
  const NPM_PACKAGE = 'https://registry.npmjs.org/@astrale-os/ui'
4
4
  const GITHUB_API = 'https://api.github.com/repos/astrale-os/ui'
5
+ const GITHUB_WEB = 'https://github.com/astrale-os/ui'
5
6
  const RAW = 'https://raw.githubusercontent.com/astrale-os/ui'
6
7
  const MAX_DOCUMENT_BYTES = 1_048_576
7
8
  const MAX_REGISTRY_DOCUMENTS = 100
8
9
 
9
10
  type Fetch = typeof fetch
11
+ class HttpStatusError extends Error {
12
+ constructor(readonly status: number) {
13
+ super('HTTP ' + status)
14
+ }
15
+ }
10
16
  type RegistrySource = {
11
17
  name?: string
12
18
  homepage?: string
@@ -23,8 +29,24 @@ async function json<T>(fetcher: Fetch, url: string, label: string): Promise<T> {
23
29
  cause,
24
30
  })
25
31
  }
32
+ const body = await responseText(response, label)
33
+ try {
34
+ return JSON.parse(body) as T
35
+ } catch (cause) {
36
+ throw new UiError('UI_REGISTRY_UNAVAILABLE', label + ' returned malformed JSON.', undefined, {
37
+ cause,
38
+ })
39
+ }
40
+ }
41
+
42
+ async function responseText(response: Response, label: string): Promise<string> {
26
43
  if (!response.ok) {
27
- throw new UiError('UI_REGISTRY_UNAVAILABLE', label + ' returned HTTP ' + response.status + '.')
44
+ throw new UiError(
45
+ 'UI_REGISTRY_UNAVAILABLE',
46
+ label + ' returned HTTP ' + response.status + '.',
47
+ undefined,
48
+ { cause: new HttpStatusError(response.status) },
49
+ )
28
50
  }
29
51
  const declaredLength = Number(response.headers.get('content-length'))
30
52
  if (Number.isFinite(declaredLength) && declaredLength > MAX_DOCUMENT_BYTES) {
@@ -52,13 +74,7 @@ async function json<T>(fetcher: Fetch, url: string, label: string): Promise<T> {
52
74
  body.set(chunk, offset)
53
75
  offset += chunk.byteLength
54
76
  }
55
- try {
56
- return JSON.parse(new TextDecoder().decode(body)) as T
57
- } catch (cause) {
58
- throw new UiError('UI_REGISTRY_UNAVAILABLE', label + ' returned malformed JSON.', undefined, {
59
- cause,
60
- })
61
- }
77
+ return new TextDecoder().decode(body)
62
78
  }
63
79
 
64
80
  export async function resolveUiRelease(
@@ -76,11 +92,19 @@ export async function resolveUiRelease(
76
92
  throw new UiError('UI_REGISTRY_UNAVAILABLE', 'Invalid UI beta release version: ' + version)
77
93
  }
78
94
  const ref = 'v' + version
79
- const reference = await json<{
80
- object: { type: 'commit' | 'tag'; sha: string; url: string }
81
- }>(fetcher, GITHUB_API + '/git/ref/tags/' + encodeURIComponent(ref), 'UI ref ' + ref)
82
- const commit =
83
- reference.object.type === 'commit'
95
+ const commit = await resolveReleaseCommit(ref, fetcher)
96
+ if (!/^[0-9a-f]{40}$/u.test(commit)) {
97
+ throw new UiError('UI_REGISTRY_UNAVAILABLE', 'UI ref ' + ref + ' did not resolve to a commit.')
98
+ }
99
+ return readUiReleaseSnapshot({ version, ref, commit }, fetcher)
100
+ }
101
+
102
+ async function resolveReleaseCommit(ref: string, fetcher: Fetch): Promise<string> {
103
+ try {
104
+ const reference = await json<{
105
+ object: { type: 'commit' | 'tag'; sha: string; url: string }
106
+ }>(fetcher, GITHUB_API + '/git/ref/tags/' + encodeURIComponent(ref), 'UI ref ' + ref)
107
+ return reference.object.type === 'commit'
84
108
  ? reference.object.sha
85
109
  : (
86
110
  await json<{ object: { sha: string } }>(
@@ -89,10 +113,41 @@ export async function resolveUiRelease(
89
113
  'annotated UI tag ' + ref,
90
114
  )
91
115
  ).object.sha
92
- if (!/^[0-9a-f]{40}$/u.test(commit)) {
116
+ } catch (cause) {
117
+ const status =
118
+ cause instanceof UiError && cause.cause instanceof HttpStatusError
119
+ ? cause.cause.status
120
+ : undefined
121
+ if (status !== 403 && status !== 429 && !(status !== undefined && status >= 500)) throw cause
122
+ }
123
+
124
+ const label = 'UI release ' + ref
125
+ let response: Response
126
+ try {
127
+ response = await fetcher(GITHUB_WEB + '/releases/tag/' + encodeURIComponent(ref), {
128
+ headers: { accept: 'text/html' },
129
+ })
130
+ } catch (cause) {
131
+ throw new UiError('UI_REGISTRY_UNAVAILABLE', 'Unable to reach ' + label + '.', undefined, {
132
+ cause,
133
+ })
134
+ }
135
+ const html = await responseText(response, label)
136
+ const marker = 'href="/astrale-os/ui/tree/' + ref + '"'
137
+ const markerOffset = html.lastIndexOf(marker)
138
+ const releaseHeader = markerOffset < 0 ? '' : html.slice(markerOffset, markerOffset + 8_192)
139
+ const commits = new Set(
140
+ [
141
+ ...releaseHeader.matchAll(
142
+ /data-hovercard-type=["']commit["'][^>]+href=["']\/astrale-os\/ui\/commit\/([0-9a-f]{40})["']/gu,
143
+ ),
144
+ ].map((match) => match[1]),
145
+ )
146
+ const commit = commits.size === 1 ? commits.values().next().value : undefined
147
+ if (!commit) {
93
148
  throw new UiError('UI_REGISTRY_UNAVAILABLE', 'UI ref ' + ref + ' did not resolve to a commit.')
94
149
  }
95
- return readUiReleaseSnapshot({ version, ref, commit }, fetcher)
150
+ return commit
96
151
  }
97
152
 
98
153
  export async function readUiReleaseSnapshot(