@drawcall/market 0.6.12 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/client.d.ts.map +1 -1
  2. package/dist/client.js +7 -4
  3. package/dist/client.js.map +1 -1
  4. package/dist/commands/install.d.ts.map +1 -1
  5. package/dist/commands/install.js +6 -4
  6. package/dist/commands/install.js.map +1 -1
  7. package/dist/commands/preview.d.ts +2 -14
  8. package/dist/commands/preview.d.ts.map +1 -1
  9. package/dist/commands/preview.js +4 -8
  10. package/dist/commands/preview.js.map +1 -1
  11. package/dist/commands/search.d.ts.map +1 -1
  12. package/dist/commands/search.js +3 -0
  13. package/dist/commands/search.js.map +1 -1
  14. package/dist/commands/upload.d.ts.map +1 -1
  15. package/dist/commands/upload.js +5 -6
  16. package/dist/commands/upload.js.map +1 -1
  17. package/dist/install.d.ts.map +1 -1
  18. package/dist/install.js +11 -12
  19. package/dist/install.js.map +1 -1
  20. package/dist/pack.d.ts.map +1 -1
  21. package/dist/pack.js +10 -17
  22. package/dist/pack.js.map +1 -1
  23. package/dist/refs.d.ts +20 -0
  24. package/dist/refs.d.ts.map +1 -0
  25. package/dist/refs.js +25 -0
  26. package/dist/refs.js.map +1 -0
  27. package/dist/resolve.d.ts +3 -0
  28. package/dist/resolve.d.ts.map +1 -1
  29. package/dist/resolve.js +27 -42
  30. package/dist/resolve.js.map +1 -1
  31. package/dist/schemas.d.ts +5 -0
  32. package/dist/schemas.d.ts.map +1 -1
  33. package/dist/schemas.js +8 -0
  34. package/dist/schemas.js.map +1 -1
  35. package/dist/v1/contract.d.ts +15 -2
  36. package/dist/v1/contract.d.ts.map +1 -1
  37. package/dist/v1/contract.js +5 -2
  38. package/dist/v1/contract.js.map +1 -1
  39. package/dist/v1/index.d.ts +1 -1
  40. package/dist/v1/index.d.ts.map +1 -1
  41. package/dist/v1/index.js +1 -1
  42. package/dist/v1/index.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/client.ts +7 -4
  45. package/src/commands/install.ts +6 -4
  46. package/src/commands/preview.ts +10 -24
  47. package/src/commands/search.ts +3 -0
  48. package/src/commands/upload.ts +5 -6
  49. package/src/install.ts +12 -15
  50. package/src/pack.ts +16 -22
  51. package/src/refs.ts +44 -0
  52. package/src/resolve.ts +44 -61
  53. package/src/schemas.ts +9 -0
  54. package/src/v1/contract.ts +17 -2
  55. package/src/v1/index.ts +7 -1
@@ -6,6 +6,7 @@ import { findInstallRoot, install as runInstall, type InstallRootRequest } from
6
6
  import { packageJsonAssetDependencies } from '../package-json.js'
7
7
  import { assetDependencyRange, assetNameSchema, type AssetDependencies } from '../schemas.js'
8
8
  import { getCliClient } from '../cli-client.js'
9
+ import { lookupAssets } from '../refs.js'
9
10
  import { installResult } from '../output.js'
10
11
  import type { MarketClient } from '../client.js'
11
12
 
@@ -82,8 +83,7 @@ export async function resolveArg(
82
83
  )
83
84
  }
84
85
 
85
- const existing = await client.asset.exact({
86
- name: parsed.name,
86
+ const [existing] = await lookupAssets(client.asset, [{ name: parsed.name }], {
87
87
  includeUnapproved,
88
88
  ...(key ? { key } : {}),
89
89
  })
@@ -91,13 +91,15 @@ export async function resolveArg(
91
91
  return {
92
92
  name: parsed.name,
93
93
  range: parsed.range,
94
- saveRange: parsed.explicitRange ? parsed.range : `^${existing.latestVersion}`,
94
+ saveRange: parsed.explicitRange ? parsed.range : `^${existing.version}`,
95
95
  save: true,
96
96
  }
97
97
  }
98
98
 
99
99
  if (!includeUnapproved) {
100
- const hidden = await client.asset.exact({ name: parsed.name, includeUnapproved: true })
100
+ const [hidden] = await lookupAssets(client.asset, [{ name: parsed.name }], {
101
+ includeUnapproved: true,
102
+ })
101
103
  if (hidden) {
102
104
  throw new Error(
103
105
  `Asset "${parsed.name}" exists but has no approved versions. Re-run with \`--unapproved\` if you have access.`,
@@ -2,7 +2,9 @@ import * as fs from 'fs/promises'
2
2
  import * as path from 'path'
3
3
  import ora from 'ora'
4
4
  import { getCliClient } from '../cli-client.js'
5
+ import type { RefsReader } from '../refs.js'
5
6
  import { previewResult } from '../output.js'
7
+ import { lookupAssets } from '../refs.js'
6
8
  import { assetNameSchema, semverSchema } from '../schemas.js'
7
9
 
8
10
  export interface PreviewCommandOptions {
@@ -11,22 +13,8 @@ export interface PreviewCommandOptions {
11
13
  baseUrl?: string
12
14
  }
13
15
 
14
- interface PreviewAsset {
15
- latestVersion: string
16
- version?: string
17
- previewUrl: string | null
18
- }
19
-
20
- interface PreviewClient {
21
- exact(input: {
22
- name: string
23
- version?: string
24
- includeUnapproved: boolean
25
- }): Promise<PreviewAsset | null>
26
- }
27
-
28
16
  export async function resolvePreviewRef(
29
- client: PreviewClient,
17
+ client: RefsReader,
30
18
  ref: string,
31
19
  includeUnapproved: boolean,
32
20
  ): Promise<{ name: string; version: string; previewUrl: string }> {
@@ -34,11 +22,11 @@ export async function resolvePreviewRef(
34
22
  const name = assetNameSchema.parse(separator === -1 ? ref : ref.slice(0, separator))
35
23
  const requestedVersion =
36
24
  separator === -1 ? undefined : semverSchema.parse(ref.slice(separator + 1))
37
- const asset = await client.exact({
38
- name,
39
- ...(requestedVersion ? { version: requestedVersion } : {}),
40
- includeUnapproved,
41
- })
25
+ const [asset] = await lookupAssets(
26
+ client,
27
+ [{ name, ...(requestedVersion ? { version: requestedVersion } : {}) }],
28
+ { includeUnapproved },
29
+ )
42
30
 
43
31
  if (!asset) {
44
32
  throw new Error(`Asset "${ref}" not found`)
@@ -46,13 +34,11 @@ export async function resolvePreviewRef(
46
34
  if (!asset.previewUrl) {
47
35
  throw new Error(`Asset "${ref}" has no preview image`)
48
36
  }
49
-
50
- const version = asset.version ?? asset.latestVersion
51
- if (requestedVersion && version !== requestedVersion) {
37
+ if (requestedVersion && asset.version !== requestedVersion) {
52
38
  throw new Error(`Market API did not return requested version "${ref}"`)
53
39
  }
54
40
 
55
- return { name, version, previewUrl: asset.previewUrl }
41
+ return { name, version: asset.version, previewUrl: asset.previewUrl }
56
42
  }
57
43
 
58
44
  export async function previewCommand(ref: string, opts: PreviewCommandOptions): Promise<void> {
@@ -33,6 +33,9 @@ export async function searchCommand(query: string, opts: SearchCommandOptions):
33
33
  })
34
34
  .finally(() => spinner.stop())
35
35
 
36
+ // The collection read is a union (search | refs); this call always searches.
37
+ if (!('items' in results)) throw new Error('Search unexpectedly returned a refs result.')
38
+
36
39
  if (opts.json) {
37
40
  // Stable machine-readable shape for scripts; the human-readable lines below may change freely.
38
41
  console.log(
@@ -1,4 +1,5 @@
1
1
  import ora from 'ora'
2
+ import { lookupAssets } from '../refs.js'
2
3
  import semver from 'semver'
3
4
  import { downloadAssetZipBytes } from '../client.js'
4
5
  import { getCliClient } from '../cli-client.js'
@@ -75,9 +76,7 @@ export async function uploadCommand(
75
76
  if (!profile) {
76
77
  throw new Error('Not logged in. Run `market login` first.')
77
78
  }
78
- const existing = await client.asset.exact({
79
- name: parsedName,
80
- type,
79
+ const [existing] = await lookupAssets(client.asset, [{ name: parsedName }], {
81
80
  includeUnapproved: true,
82
81
  })
83
82
 
@@ -88,7 +87,7 @@ export async function uploadCommand(
88
87
  if (existing) {
89
88
  const latestBytes = await downloadAssetZipBytes({
90
89
  name: parsedName,
91
- version: existing.latestVersion,
90
+ version: existing.version,
92
91
  baseUrl,
93
92
  authToken,
94
93
  })
@@ -100,12 +99,12 @@ export async function uploadCommand(
100
99
  bytesEqual(latestBytes, zip)
101
100
  ) {
102
101
  spinner.stop()
103
- console.log(unchangedUploadResult(parsedName, existing.latestVersion))
102
+ console.log(unchangedUploadResult(parsedName, existing.version))
104
103
  return
105
104
  }
106
105
  }
107
106
 
108
- const version = parsedVersion ?? nextVersion(existing?.latestVersion)
107
+ const version = parsedVersion ?? nextVersion(existing?.version)
109
108
  spinner.text = `Uploading ${parsedName}@${version}`
110
109
  const uploaded = await client.asset.uploadZip({
111
110
  name: parsedName,
package/src/install.ts CHANGED
@@ -18,6 +18,7 @@ import pMap from 'p-map'
18
18
  import type { MarketClient } from './client.js'
19
19
  import type { AssetFilesResult, AssetInstallMetadata } from './contract.js'
20
20
  import { withProjectInstallLock } from './install-lock.js'
21
+ import { lookupAssets } from './refs.js'
21
22
  import { md5, resolveDependencyLayout } from './pack.js'
22
23
  import { findInstallRoot, walkWithGitignore } from './project-walk.js'
23
24
  import { packageJsonAssetDependencies, parsePackageJson, type PackageJson } from './package-json.js'
@@ -279,14 +280,10 @@ async function downloadOneAsset(
279
280
  }> {
280
281
  opts.log(`Downloading ${asset.name}@${asset.version}...`)
281
282
 
282
- // The index alone (paths + etags + fetch URLs) is enough to plan targets, adopt renames, and
283
- // emit redirect rules; file bytes are fetched afterwards, and only for files actually written.
284
- const index = await client.asset.files({
285
- name: asset.name,
286
- version: asset.version,
287
- ...(opts.key ? { key: opts.key } : {}),
288
- })
289
- const planned = index.files.flatMap((entry) => {
283
+ // The file index (paths + etags + fetch URLs) arrived with the resolution
284
+ // enough to plan targets, adopt renames, and emit redirect rules; file bytes
285
+ // are fetched afterwards, and only for files actually written.
286
+ const planned = asset.files.flatMap((entry) => {
290
287
  const normalizedPath = safeRelativePath(
291
288
  entry.path,
292
289
  () => `Zip contains an unsafe path: ${entry.path}`,
@@ -490,14 +487,14 @@ function memoizedKnownHashes(client: MarketClient): (name: string) => Promise<Se
490
487
  if (hit) return hit
491
488
  const result = (async () => {
492
489
  try {
493
- const { versions } = await client.asset.versions({ name })
494
- const recent = versions.map((entry) => entry.version).slice(-KNOWN_HASH_VERSION_CAP)
495
- const etags = await Promise.all(
496
- recent.map(async (version) =>
497
- (await client.asset.files({ name, version })).files.map((file) => file.etag),
498
- ),
490
+ const [latest] = await lookupAssets(client.asset, [{ name }])
491
+ if (!latest) return null
492
+ const recent = latest.versions.slice(-KNOWN_HASH_VERSION_CAP)
493
+ const entries = await lookupAssets(
494
+ client.asset,
495
+ recent.map((version) => ({ name, version })),
499
496
  )
500
- return new Set(etags.flat())
497
+ return new Set(entries.flatMap((entry) => entry?.files.map((file) => file.etag) ?? []))
501
498
  } catch {
502
499
  // Offline or not visible — the conservative fail-open: the caller keeps skip-and-warn.
503
500
  return null
package/src/pack.ts CHANGED
@@ -3,7 +3,6 @@ import * as fs from 'fs/promises'
3
3
  import * as path from 'path'
4
4
  import { unzipSync, zipSync } from 'fflate'
5
5
  import ignore from 'ignore'
6
- import pMap from 'p-map'
7
6
  import semver from 'semver'
8
7
  import {
9
8
  ALWAYS_IGNORED_DIRS,
@@ -14,6 +13,7 @@ import {
14
13
  type GitignoreMatcher,
15
14
  } from './project-walk.js'
16
15
  import type { MarketClient } from './client.js'
16
+ import { lookupAssets } from './refs.js'
17
17
  import type { AssetInstallMetadata } from './contract.js'
18
18
  import {
19
19
  packageJsonAssetDependencies,
@@ -102,35 +102,29 @@ const MANIFEST_VERSION_CAP = 20
102
102
  export function fileManifestForRange(asset: MarketClient['asset']): FetchFileManifest {
103
103
  return async (name, range) => {
104
104
  const exact = exactRangeVersion(range)
105
- if (exact) return fileIndexHashes(asset, name, exact)
106
-
107
- const { versions } = await asset.versions({ name })
108
- const inRange = versions
109
- .map((entry) => entry.version)
110
- .filter((version) => semver.satisfies(version, range))
111
- .sort(semver.compare)
112
- .slice(-MANIFEST_VERSION_CAP)
105
+ const inRange = exact
106
+ ? [exact]
107
+ : ((await lookupAssets(asset, [{ name }]))[0]?.versions ?? [])
108
+ .filter((version) => semver.satisfies(version, range))
109
+ .sort(semver.compare)
110
+ .slice(-MANIFEST_VERSION_CAP)
113
111
 
114
112
  // Oldest first, so a newer version's hash wins when a path recurs across versions.
113
+ const entries = await lookupAssets(
114
+ asset,
115
+ inRange.map((version) => ({ name, version })),
116
+ )
115
117
  const merged: Record<string, string> = {}
116
- for (const hashes of await pMap(inRange, (version) => fileIndexHashes(asset, name, version), {
117
- concurrency: 4,
118
- })) {
119
- Object.assign(merged, hashes)
118
+ for (const entry of entries) {
119
+ Object.assign(
120
+ merged,
121
+ Object.fromEntries((entry?.files ?? []).map((file) => [file.path, file.etag])),
122
+ )
120
123
  }
121
124
  return merged
122
125
  }
123
126
  }
124
127
 
125
- async function fileIndexHashes(
126
- asset: MarketClient['asset'],
127
- name: string,
128
- version: string,
129
- ): Promise<Record<string, string>> {
130
- const index = await asset.files({ name, version })
131
- return Object.fromEntries(index.files.map((file) => [file.path, file.etag]))
132
- }
133
-
134
128
  function exactRangeVersion(range: string): string | null {
135
129
  const candidate = range.trim().replace(/^=/u, '')
136
130
  return semverSchema.safeParse(candidate).success ? candidate : null
package/src/refs.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { MarketClient } from './client.js'
2
+ import type { AssetRefEntry } from './v1/index.js'
3
+
4
+ export interface AssetRef {
5
+ name: string
6
+ version?: string
7
+ }
8
+
9
+ /** The one call the refs read needs — lets tests fake a single function. */
10
+ export interface RefsReader {
11
+ search: MarketClient['asset']['search']
12
+ }
13
+
14
+ /** The server accepts up to 100 refs per read; larger sets chunk transparently. */
15
+ const MAX_REFS_PER_CALL = 100
16
+
17
+ /**
18
+ * The collection read in refs mode: full entries (metadata, version history,
19
+ * files with data-plane URLs) for exactly the given refs — order-preserved,
20
+ * null per miss. Version omitted resolves to the latest visible version.
21
+ */
22
+ export async function lookupAssets(
23
+ asset: RefsReader,
24
+ refs: AssetRef[],
25
+ opts: { includeUnapproved?: boolean; key?: string } = {},
26
+ ): Promise<(AssetRefEntry | null)[]> {
27
+ const chunks: AssetRef[][] = []
28
+ for (let i = 0; i < refs.length; i += MAX_REFS_PER_CALL) {
29
+ chunks.push(refs.slice(i, i + MAX_REFS_PER_CALL))
30
+ }
31
+
32
+ const results = await Promise.all(
33
+ chunks.map(async (chunk) => {
34
+ const result = await asset.search({
35
+ refs: chunk.map((ref) => (ref.version ? `${ref.name}@${ref.version}` : ref.name)).join(','),
36
+ includeUnapproved: opts.includeUnapproved ?? false,
37
+ ...(opts.key ? { key: opts.key } : {}),
38
+ })
39
+ if (!('assets' in result)) throw new Error('refs read unexpectedly returned a search result')
40
+ return result.assets
41
+ }),
42
+ )
43
+ return results.flat()
44
+ }
package/src/resolve.ts CHANGED
@@ -11,7 +11,9 @@
11
11
 
12
12
  import * as semver from 'semver'
13
13
  import type { MarketClient } from './client.js'
14
- import type { AssetExactResult } from './contract.js'
14
+ import type { AssetFileEntry } from './contract.js'
15
+ import { lookupAssets } from './refs.js'
16
+ import type { AssetRefEntry } from './v1/index.js'
15
17
  import {
16
18
  assetDependencyAlias,
17
19
  type AssetDependencyAlias,
@@ -28,6 +30,8 @@ export interface ResolvedAsset {
28
30
  npmDependencies: Record<string, string>
29
31
  assetDependencies: AssetDependencies
30
32
  skillDependencies: Record<string, string>
33
+ /** The resolved version's file index, carried from resolution so install needs no further reads. */
34
+ files: AssetFileEntry[]
31
35
  }
32
36
 
33
37
  export interface ResolveResult {
@@ -74,7 +78,7 @@ export async function resolve(
74
78
  requests: AssetRequest[],
75
79
  opts: { includeUnapproved?: boolean; key?: string } = {},
76
80
  ): Promise<ResolveResult> {
77
- const metaCache = new Map<string, AssetExactResult>()
81
+ const metaCache = new Map<string, AssetRefEntry>()
78
82
  const includeUnapproved = opts.includeUnapproved ?? false
79
83
  let exactPins = exactPinsFor(rootConstraints(requests))
80
84
  const seenPinSets = new Set<string>()
@@ -122,7 +126,7 @@ async function resolvePass(
122
126
  requests: AssetRequest[],
123
127
  initialPins: ExactPins,
124
128
  includeUnapproved: boolean,
125
- metaCache: Map<string, AssetExactResult>,
129
+ metaCache: Map<string, AssetRefEntry>,
126
130
  key?: string,
127
131
  ): Promise<ResolutionPass> {
128
132
  const constraints = rootConstraints(requests)
@@ -138,19 +142,34 @@ async function resolvePass(
138
142
  const names = [...new Set(wave)].filter((name) => !resolved.has(name))
139
143
  wave = []
140
144
 
141
- const metas = await Promise.all(
142
- names.map(async (assetName) => {
143
- const constrainedExact = requiredExactVersion(assetName, constraints.get(assetName) ?? [])
144
- const exactVersion = constrainedExact ?? initialPins.get(assetName) ?? null
145
- return {
146
- assetName,
147
- exactVersion,
148
- meta: await fetchMeta(client, metaCache, assetName, exactVersion, includeUnapproved, key),
149
- }
150
- }),
151
- )
145
+ const wanted = names.map((assetName) => {
146
+ const constrainedExact = requiredExactVersion(assetName, constraints.get(assetName) ?? [])
147
+ const exactVersion = constrainedExact ?? initialPins.get(assetName) ?? null
148
+ return { assetName, exactVersion }
149
+ })
152
150
 
153
- for (const { assetName, exactVersion, meta } of metas) {
151
+ // One collection read per wave; the cache spans convergence passes, so a
152
+ // re-pass that pins already-seen versions makes no further requests.
153
+ const misses = wanted.filter(
154
+ ({ assetName, exactVersion }) => !metaCache.has(metaKey(assetName, exactVersion)),
155
+ )
156
+ const fetched = await lookupAssets(
157
+ client,
158
+ misses.map(({ assetName, exactVersion }) => ({
159
+ name: assetName,
160
+ version: exactVersion ?? undefined,
161
+ })),
162
+ { includeUnapproved, key },
163
+ )
164
+ misses.forEach(({ assetName, exactVersion }, index) => {
165
+ const entry = fetched[index]
166
+ if (!entry) return
167
+ metaCache.set(metaKey(assetName, exactVersion), entry)
168
+ metaCache.set(metaKey(assetName, entry.version), entry)
169
+ })
170
+
171
+ for (const { assetName, exactVersion } of wanted) {
172
+ const meta = metaCache.get(metaKey(assetName, exactVersion)) ?? null
154
173
  if (!meta) {
155
174
  if (exactVersion) {
156
175
  throw new ResolutionError(
@@ -166,12 +185,13 @@ async function resolvePass(
166
185
  throw new ResolutionError(`Asset "${assetName}" has no approved versions.`)
167
186
  }
168
187
 
169
- const selectedVersion = exactVersion
170
- ? exactMetadataVersion(meta, assetName, exactVersion)
171
- : latestMetadataVersion(meta)
172
- // The meta demonstrably answers for this version; caching it under the exact key makes the
173
- // convergence re-pass (which pins every asset) resolve without a single further request.
174
- metaCache.set(`${assetName}@${selectedVersion}`, meta)
188
+ if (exactVersion && meta.version !== exactVersion) {
189
+ throw new ResolutionError(
190
+ `Market API did not return requested version "${assetName}@${exactVersion}". ` +
191
+ 'Refusing to fall back to a different version.',
192
+ )
193
+ }
194
+ const selectedVersion = meta.version
175
195
  const assetDependencies = meta.assetDependencies
176
196
  resolved.set(assetName, {
177
197
  name: assetName,
@@ -181,6 +201,7 @@ async function resolvePass(
181
201
  npmDependencies: meta.npmDependencies,
182
202
  assetDependencies,
183
203
  skillDependencies: meta.skillDependencies,
204
+ files: meta.files,
184
205
  })
185
206
 
186
207
  for (const [dependencyName, dependencyValue] of Object.entries(assetDependencies)) {
@@ -293,28 +314,8 @@ function pinSignature(pins: ExactPins): string {
293
314
  return JSON.stringify([...pins].sort(([left], [right]) => left.localeCompare(right)))
294
315
  }
295
316
 
296
- async function fetchMeta(
297
- client: MarketClient['asset'],
298
- cache: Map<string, AssetExactResult>,
299
- name: string,
300
- version: string | null,
301
- includeUnapproved: boolean,
302
- key?: string,
303
- ): Promise<AssetExactResult | null> {
304
- const cacheKey = version ? `${name}@${version}` : name
305
- const cached = cache.get(cacheKey)
306
- if (cached) return cached
307
- // The key is sent with every lookup in the tree; it only unlocks the asset it belongs to and is
308
- // inert for the rest, so no per-asset routing is needed.
309
- const meta = await client.exact({
310
- name,
311
- ...(version ? { version } : {}),
312
- includeUnapproved,
313
- ...(key ? { key } : {}),
314
- })
315
- if (meta) cache.set(cacheKey, meta)
316
- return meta
317
- }
317
+ const metaKey = (name: string, version: string | null): string =>
318
+ version ? `${name}@${version}` : name
318
319
 
319
320
  function requiredExactVersion(assetName: string, constraints: Constraint[]): string | null {
320
321
  const exactVersions = new Set(
@@ -335,24 +336,6 @@ function exactVersion(range: string): string | null {
335
336
  return semverSchema.safeParse(candidate).success ? candidate : null
336
337
  }
337
338
 
338
- function exactMetadataVersion(
339
- meta: AssetExactResult,
340
- assetName: string,
341
- requestedVersion: string,
342
- ): string {
343
- if (meta.version === requestedVersion) return meta.version
344
- throw new ResolutionError(
345
- `Market API did not return requested version "${assetName}@${requestedVersion}". ` +
346
- 'Refusing to fall back to a different version.',
347
- )
348
- }
349
-
350
- function latestMetadataVersion(meta: AssetExactResult): string {
351
- // Older workers do not return `version`. Range requests can retain their established latest-
352
- // version behavior; exact requests instead fail closed in exactMetadataVersion above.
353
- return meta.version ?? meta.latestVersion
354
- }
355
-
356
339
  /**
357
340
  * Merge npm dependency ranges from all resolved assets.
358
341
  * For each package, check that all declared ranges are compatible
package/src/schemas.ts CHANGED
@@ -121,6 +121,15 @@ export const exactAssetSchema = z.object({
121
121
  key: accessKeySchema.optional(),
122
122
  })
123
123
 
124
+ // The refs mode of the collection read: `?refs=name[@version],…` returns exactly those assets —
125
+ // full entries with files and version history — order-preserved, null per miss. Reads are plural
126
+ // by default; a single asset is the plural of one.
127
+ export const assetRefsSchema = z.object({
128
+ refs: z.string().min(1).max(4000),
129
+ includeUnapproved: z.boolean().default(false),
130
+ key: accessKeySchema.optional(),
131
+ })
132
+
124
133
  export const uploadZipSchema = z.object({
125
134
  name: assetNameSchema,
126
135
  type: assetTypeSchema,
@@ -16,6 +16,7 @@ import {
16
16
  agentStartSchema,
17
17
  agentStatusSchema,
18
18
  assetFilesSchema,
19
+ assetRefsSchema,
19
20
  assetVersionsSchema,
20
21
  exactAssetSchema,
21
22
  generateAssetSchema,
@@ -37,6 +38,7 @@ export interface AssetVersionManifest {
37
38
  name: string
38
39
  type: string
39
40
  version: string
41
+ ownerId: string
40
42
  description: string | null
41
43
  approved: boolean
42
44
  access: 'public' | 'private'
@@ -48,6 +50,16 @@ export interface AssetVersionManifest {
48
50
  zipUrl: string
49
51
  }
50
52
 
53
+ /** One entry of the refs read: the version's full manifest plus the asset's version history
54
+ * (oldest first), so range selection and multi-version hashing need no further calls. */
55
+ export interface AssetRefEntry extends AssetVersionManifest {
56
+ versions: string[]
57
+ }
58
+
59
+ export interface AssetRefsResult {
60
+ assets: (AssetRefEntry | null)[]
61
+ }
62
+
51
63
  /**
52
64
  * The v1 contract: the same procedure tree as the legacy RPC surface (so call sites migrate by
53
65
  * swapping only the client), with HTTP routes attached — served as plain REST under `/api/v1`.
@@ -55,10 +67,13 @@ export interface AssetVersionManifest {
55
67
  */
56
68
  export const contract = {
57
69
  asset: {
70
+ // The one collection read, discriminated by params: `?refs=name[@version],…`
71
+ // returns exactly those assets (full entries, order-preserved, null per
72
+ // miss); everything else is a search. Reads are plural by default.
58
73
  search: oc
59
74
  .route({ method: 'GET', path: '/assets' })
60
- .input(listAssetsSchema)
61
- .output(z.custom<PaginatedList<AssetSearchResult>>()),
75
+ .input(z.union([assetRefsSchema, listAssetsSchema]))
76
+ .output(z.custom<PaginatedList<AssetSearchResult> | AssetRefsResult>()),
62
77
 
63
78
  exact: oc
64
79
  .route({ method: 'GET', path: '/assets/{name}' })
package/src/v1/index.ts CHANGED
@@ -1,2 +1,8 @@
1
- export { contract, type V1Contract, type AssetVersionManifest } from './contract.js'
1
+ export {
2
+ contract,
3
+ type V1Contract,
4
+ type AssetVersionManifest,
5
+ type AssetRefEntry,
6
+ type AssetRefsResult,
7
+ } from './contract.js'
2
8
  export { createClient, type MarketV1Client, type MarketV1ClientOptions } from './client.js'