@ouraihub/aether-core 0.1.0 → 0.1.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.
@@ -15,7 +15,10 @@ export const PROVIDERS: readonly ProviderSpec[] = [
15
15
  name: '智谱 Zhipu',
16
16
  description: 'Web search',
17
17
  basicAuth: false,
18
- search: { defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4/web_search', keyMode: 'required' },
18
+ search: {
19
+ defaultBaseUrl: 'https://open.bigmodel.cn/api/paas/v4/web_search',
20
+ keyMode: 'required',
21
+ },
19
22
  },
20
23
  {
21
24
  id: 'tavily',
@@ -98,7 +101,10 @@ export const PROVIDERS: readonly ProviderSpec[] = [
98
101
  fetch: { defaultBaseUrl: '', keyMode: 'none' },
99
102
  },
100
103
  ]
101
- export const DEFAULT_PROVIDERS: Readonly<Record<SearchCap, string>> = { search: 'exa-mcp', fetch: 'jina' }
104
+ export const DEFAULT_PROVIDERS: Readonly<Record<SearchCap, string>> = {
105
+ search: 'exa-mcp',
106
+ fetch: 'jina',
107
+ }
102
108
  export const defaultPreferences = (): SearchPreferences => ({
103
109
  maxResults: 5,
104
110
  compression: 'none',
@@ -141,7 +147,12 @@ export function targetUrl(value: string): URL {
141
147
  return url
142
148
  }
143
149
  export function normalizeRule(value: unknown): string {
144
- if (typeof value !== 'string' || !value || [...value].length > 256 || /[\s[\]\\^{}|#]/u.test(value))
150
+ if (
151
+ typeof value !== 'string' ||
152
+ !value ||
153
+ [...value].length > 256 ||
154
+ /[\s[\]\\^{}|#]/u.test(value)
155
+ )
145
156
  throw badRequest('blacklist', 'invalid')
146
157
  for (const character of value) {
147
158
  const code = character.codePointAt(0)
@@ -150,25 +161,39 @@ export function normalizeRule(value: unknown): string {
150
161
  const parts = value.match(/^(https?):\/\/([^/]+)(\/.*)?$/i)
151
162
  const hostPort = parts ? (parts[2] ?? '') : value
152
163
  const match = hostPort.match(/^([^:]+)(?::([0-9]+))?$/)
153
- if (!match || (!parts && match[2]) || (match[2] && (Number(match[2]) < 1 || Number(match[2]) > 65535)))
164
+ if (
165
+ !match ||
166
+ (!parts && match[2]) ||
167
+ (match[2] && (Number(match[2]) < 1 || Number(match[2]) > 65535))
168
+ )
154
169
  throw badRequest('blacklist', 'invalid')
155
170
  const host = (match[1] ?? '').toLowerCase().replace(/\.$/, '')
156
171
  if (
157
172
  host.length > 253 ||
158
173
  host
159
174
  .split('.')
160
- .some((label) => !label || label.length > 63 || !/^[a-z0-9*?](?:[a-z0-9*?-]*[a-z0-9*?])?$/u.test(label))
175
+ .some(
176
+ (label) =>
177
+ !label || label.length > 63 || !/^[a-z0-9*?](?:[a-z0-9*?-]*[a-z0-9*?])?$/u.test(label),
178
+ )
161
179
  )
162
180
  throw badRequest('blacklist', 'invalid')
163
181
  const scheme = parts?.[1]?.toLowerCase()
164
182
  const port =
165
- match[2] && !((scheme === 'https' && match[2] === '443') || (scheme === 'http' && match[2] === '80'))
183
+ match[2] &&
184
+ !((scheme === 'https' && match[2] === '443') || (scheme === 'http' && match[2] === '80'))
166
185
  ? ':' + match[2]
167
186
  : ''
168
187
  return parts ? scheme + '://' + host + port + (parts[3] ?? '/') : host
169
188
  }
170
189
  export function validatePreferences(value: unknown): SearchPreferences {
171
- const input = fields(value, ['maxResults', 'compression', 'cutoffLimit', 'blacklist', 'modelToolsPreferred'])
190
+ const input = fields(value, [
191
+ 'maxResults',
192
+ 'compression',
193
+ 'cutoffLimit',
194
+ 'blacklist',
195
+ 'modelToolsPreferred',
196
+ ])
172
197
  const { maxResults, compression, cutoffLimit, blacklist, modelToolsPreferred } = input
173
198
  if (
174
199
  typeof maxResults !== 'number' ||
@@ -238,7 +263,10 @@ export function truncate(text: string, limit: number): string {
238
263
  export function cutoff(text: string, preferences: SearchPreferences): string {
239
264
  return preferences.compression === 'cutoff' ? truncate(text, preferences.cutoffLimit) : text
240
265
  }
241
- export function processHits(hits: readonly SearchHit[], preferences: SearchPreferences): SearchHit[] {
266
+ export function processHits(
267
+ hits: readonly SearchHit[],
268
+ preferences: SearchPreferences,
269
+ ): SearchHit[] {
242
270
  const seen = new Set<string>()
243
271
  const result: SearchHit[] = []
244
272
  for (const hit of hits) {
@@ -251,7 +279,11 @@ export function processHits(hits: readonly SearchHit[], preferences: SearchPrefe
251
279
  }
252
280
  if (seen.has(url)) continue
253
281
  seen.add(url)
254
- result.push({ title: truncate(hit.title || url, 500), url, snippet: cutoff(hit.snippet, preferences) })
282
+ result.push({
283
+ title: truncate(hit.title || url, 500),
284
+ url,
285
+ snippet: cutoff(hit.snippet, preferences),
286
+ })
255
287
  if (result.length >= preferences.maxResults) break
256
288
  }
257
289
  return result
@@ -262,6 +294,7 @@ export function validateQuery(value: string): string {
262
294
  return query
263
295
  }
264
296
  export function validateTarget(value: string): string {
265
- if (new TextEncoder().encode(stringValue(value, 'url')).length > 8192) throw badRequest('url', 'too_long', 8192)
297
+ if (new TextEncoder().encode(stringValue(value, 'url')).length > 8192)
298
+ throw badRequest('url', 'too_long', 8192)
266
299
  return targetUrl(value.trim()).href
267
300
  }
@@ -53,7 +53,16 @@ function collect(items: readonly unknown[]): SearchHit[] {
53
53
  hits.push({
54
54
  title: text(item, 'title', 'name') || url,
55
55
  url,
56
- snippet: text(item, 'content', 'text', 'snippet', 'description', 'excerpts', 'summary', 'markdown'),
56
+ snippet: text(
57
+ item,
58
+ 'content',
59
+ 'text',
60
+ 'snippet',
61
+ 'description',
62
+ 'excerpts',
63
+ 'summary',
64
+ 'markdown',
65
+ ),
57
66
  })
58
67
  }
59
68
  return hits
@@ -116,7 +125,10 @@ export function parseMcp(body: string): SearchHit[] {
116
125
  })),
117
126
  )
118
127
  }
119
- function headers(key: string | undefined, kind: 'bearer' | 'api' = 'bearer'): Record<string, string> {
128
+ function headers(
129
+ key: string | undefined,
130
+ kind: 'bearer' | 'api' = 'bearer',
131
+ ): Record<string, string> {
120
132
  return {
121
133
  accept: 'application/json',
122
134
  'content-type': 'application/json',
@@ -155,7 +167,9 @@ export function searchRequest(
155
167
  init = {
156
168
  headers: {
157
169
  accept: 'application/json',
158
- ...(config.basicAuthUsername ? { authorization: basic(config.basicAuthUsername, key ?? '') } : {}),
170
+ ...(config.basicAuthUsername
171
+ ? { authorization: basic(config.basicAuthUsername, key ?? '') }
172
+ : {}),
159
173
  },
160
174
  }
161
175
  break
@@ -194,7 +208,10 @@ export function searchRequest(
194
208
  break
195
209
  case 'parallel':
196
210
  url += '/v1/search'
197
- init = post({ objective: query, search_queries: [query], advanced_settings: { max_results: count } }, 'api')
211
+ init = post(
212
+ { objective: query, search_queries: [query], advanced_settings: { max_results: count } },
213
+ 'api',
214
+ )
198
215
  break
199
216
  case 'jina':
200
217
  url += '/' + encodeURIComponent(query)
@@ -205,7 +222,10 @@ export function searchRequest(
205
222
  init = post({ query, limit: count, scrapeOptions: { formats: ['markdown'] } })
206
223
  break
207
224
  default:
208
- throw new KitFailure('bad_request', 'Unsupported search provider', { field: 'provider', reason: 'unsupported' })
225
+ throw new KitFailure('bad_request', 'Unsupported search provider', {
226
+ field: 'provider',
227
+ reason: 'unsupported',
228
+ })
209
229
  }
210
230
  return { url, init }
211
231
  }
@@ -226,16 +246,26 @@ export function fetchRequest(
226
246
  },
227
247
  }
228
248
  case 'jina':
229
- return { url: base + '/' + target, init: { headers: { ...headers(key), 'x-retain-images': 'none' } } }
249
+ return {
250
+ url: base + '/' + target,
251
+ init: { headers: { ...headers(key), 'x-retain-images': 'none' } },
252
+ }
230
253
  case 'firecrawl':
231
254
  return {
232
255
  url: base + '/v2/scrape',
233
- init: { method: 'POST', headers: headers(key), body: JSON.stringify({ url: target, formats: ['markdown'] }) },
256
+ init: {
257
+ method: 'POST',
258
+ headers: headers(key),
259
+ body: JSON.stringify({ url: target, formats: ['markdown'] }),
260
+ },
234
261
  }
235
262
  case 'fetch':
236
263
  return { url: target, init: { method: 'GET', headers: { accept: 'text/html, text/plain' } } }
237
264
  default:
238
- throw new KitFailure('bad_request', 'Unsupported fetch provider', { field: 'provider', reason: 'unsupported' })
265
+ throw new KitFailure('bad_request', 'Unsupported fetch provider', {
266
+ field: 'provider',
267
+ reason: 'unsupported',
268
+ })
239
269
  }
240
270
  }
241
271
  export function parsePage(provider: string, body: string, target: string): SearchPage {
@@ -246,7 +276,10 @@ export function parsePage(provider: string, body: string, target: string): Searc
246
276
  ?.replace(/<[^>]*>/gu, ' ')
247
277
  .replace(/\s+/gu, ' ')
248
278
  .trim() || target
249
- const visible = body.replace(/<!--[\s\S]*?-->|<(script|style|noscript|head|title)\b[^>]*>[\s\S]*?<\/\1\s*>/giu, ' ')
279
+ const visible = body.replace(
280
+ /<!--[\s\S]*?-->|<(script|style|noscript|head|title)\b[^>]*>[\s\S]*?<\/\1\s*>/giu,
281
+ ' ',
282
+ )
250
283
  const documentBody = visible.match(/<body\b[^>]*>([\s\S]*?)(?:<\/body\s*>|$)/iu)?.[1] ?? visible
251
284
  const content = documentBody
252
285
  .replace(/<[^>]*>/gu, ' ')
@@ -14,15 +14,25 @@ export interface FileIdentity {
14
14
  /** docs14 §3.1: validated text and unique files in ascending path order are prerequisites.
15
15
  * No trimming, newline normalization, sorting or case conversion is performed here.
16
16
  */
17
- export async function digest(content: ContentIdentity, files: readonly FileIdentity[] = []): Promise<string> {
17
+ export async function digest(
18
+ content: ContentIdentity,
19
+ files: readonly FileIdentity[] = [],
20
+ ): Promise<string> {
18
21
  const encoder = new TextEncoder()
19
- const fields: (string | null)[] = [content.name, content.description, content.version, content.body]
22
+ const fields: (string | null)[] = [
23
+ content.name,
24
+ content.description,
25
+ content.version,
26
+ content.body,
27
+ ]
20
28
  if (content.license !== null || files.length !== 0) {
21
29
  fields.push('package', content.license)
22
30
  for (const file of files) fields.push(file.path, file.sha256)
23
31
  }
24
32
  const encoded = fields.map((field) => (field === null ? null : encoder.encode(field)))
25
- const bytes = new Uint8Array(encoded.reduce((size, field) => size + (field === null ? 1 : 9 + field.length), 0))
33
+ const bytes = new Uint8Array(
34
+ encoded.reduce((size, field) => size + (field === null ? 1 : 9 + field.length), 0),
35
+ )
26
36
  const view = new DataView(bytes.buffer)
27
37
  let offset = 0
28
38
  for (const field of encoded) {
@@ -1,6 +1,11 @@
1
1
  export { digest, sha256 } from './content.js'
2
2
  export type { ContentIdentity, FileIdentity } from './content.js'
3
- export { parseManualSave, parseSkillMarkdown, parseSkillSource, validateSkillContent } from './validation.js'
3
+ export {
4
+ parseManualSave,
5
+ parseSkillMarkdown,
6
+ parseSkillSource,
7
+ validateSkillContent,
8
+ } from './validation.js'
4
9
  export { readSkillSource } from './package.js'
5
10
  export type { PackageFile, SkillPackage } from './package.js'
6
11
  export type { BuiltinSkill, SkillSnapshot, SkillRecord, SkillCatalog } from './service.js'
@@ -1,7 +1,12 @@
1
1
  import type { SkillFileKind } from '@ouraihub/aether-contracts'
2
2
  import { badRequest, KitFailure } from '../error.js'
3
3
  import { sha256 } from './content.js'
4
- import { checkSecrets, parseSkillMarkdown, parseSkillSource, type ParsedSkill } from './validation.js'
4
+ import {
5
+ checkSecrets,
6
+ parseSkillMarkdown,
7
+ parseSkillSource,
8
+ type ParsedSkill,
9
+ } from './validation.js'
5
10
 
6
11
  export interface PackageFile {
7
12
  readonly path: string
@@ -40,7 +45,13 @@ function normalize(path: string): string {
40
45
  }
41
46
  function kind(path: string): SkillFileKind | null {
42
47
  const top = path.split('/')[0]
43
- return top === 'scripts' ? 'script' : top === 'references' ? 'reference' : top === 'assets' ? 'asset' : null
48
+ return top === 'scripts'
49
+ ? 'script'
50
+ : top === 'references'
51
+ ? 'reference'
52
+ : top === 'assets'
53
+ ? 'asset'
54
+ : null
44
55
  }
45
56
  function checkPath(path: string): void {
46
57
  if (!path.split('/').every((part) => /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/u.test(part)))
@@ -82,7 +93,8 @@ function entries(bytes: Uint8Array): Entry[] {
82
93
  const result: Entry[] = []
83
94
  const paths = new Set<string>()
84
95
  for (let index = 0; index < count; index++) {
85
- if (offset + 46 > centralEnd || view.getUint32(offset, true) !== 0x02014b50) throw badRequest('zip', 'invalid')
96
+ if (offset + 46 > centralEnd || view.getUint32(offset, true) !== 0x02014b50)
97
+ throw badRequest('zip', 'invalid')
86
98
  const flags = view.getUint16(offset + 8, true)
87
99
  if ((flags & 1) !== 0 || (flags & 64) !== 0) throw badRequest('zip', 'encrypted')
88
100
  const mode = view.getUint32(offset + 38, true) >>> 16
@@ -91,7 +103,8 @@ function entries(bytes: Uint8Array): Entry[] {
91
103
  const extraLength = view.getUint16(offset + 30, true)
92
104
  const commentLength = view.getUint16(offset + 32, true)
93
105
  const next = offset + 46 + nameLength + extraLength + commentLength
94
- if (next > centralEnd || view.getUint16(offset + 34, true) !== 0) throw badRequest('zip', 'invalid')
106
+ if (next > centralEnd || view.getUint16(offset + 34, true) !== 0)
107
+ throw badRequest('zip', 'invalid')
95
108
  const rawPath = decode(bytes.subarray(offset + 46, offset + 46 + nameLength), 'zip')
96
109
  const path = normalize(rawPath)
97
110
  if (paths.has(path)) throw badRequest('zip', 'duplicate_path')
@@ -182,7 +195,10 @@ export async function readSkillSource(value: unknown): Promise<SkillPackage> {
182
195
  const fileEntries = allEntries.filter((entry) => !entry.directory)
183
196
  const first = fileEntries[0]?.path
184
197
  const prefix = first?.includes('/') ? first.slice(0, first.indexOf('/') + 1) : null
185
- const strip = prefix !== null && fileEntries.every((entry) => entry.path.startsWith(prefix)) ? prefix.length : 0
198
+ const strip =
199
+ prefix !== null && fileEntries.every((entry) => entry.path.startsWith(prefix))
200
+ ? prefix.length
201
+ : 0
186
202
  const files: PackageFile[] = []
187
203
  const unsupported: string[] = []
188
204
  let raw: string | undefined
@@ -207,7 +223,8 @@ export async function readSkillSource(value: unknown): Promise<SkillPackage> {
207
223
  const limit = Math.min(ownLimit, TOTAL - total)
208
224
  const field = limit < ownLimit ? 'total' : path
209
225
  const content = await inflate(bytes, entry, limit, field)
210
- if (content.length !== entry.size || crc32(content) !== entry.crc) throw badRequest('zip', 'invalid')
226
+ if (content.length !== entry.size || crc32(content) !== entry.crc)
227
+ throw badRequest('zip', 'invalid')
211
228
  total += content.length
212
229
  let value: string | undefined
213
230
  if (fileKind === 'asset') {
@@ -223,7 +240,12 @@ export async function readSkillSource(value: unknown): Promise<SkillPackage> {
223
240
  if (license !== null) throw badRequest(path, 'duplicate')
224
241
  license = value ?? null
225
242
  } else if (fileKind !== null)
226
- files.push({ path, kind: fileKind, bytes: Array.from(content), sha256: await sha256(content) })
243
+ files.push({
244
+ path,
245
+ kind: fileKind,
246
+ bytes: Array.from(content),
247
+ sha256: await sha256(content),
248
+ })
227
249
  }
228
250
  if (unsupported.length) throw badRequest(unsupported.sort().join(', '), 'unsupported_files')
229
251
  if (raw === undefined) throw badRequest('SKILL.md', 'missing')
@@ -33,8 +33,12 @@ export interface SkillRecord extends SkillPackage {
33
33
  }
34
34
  export type SkillCatalog = readonly SkillRecord[]
35
35
  export const conflict = (reason: string, entity?: string): KitFailure =>
36
- new KitFailure('conflict', 'Skill conflict', { reason, ...(entity === undefined ? {} : { entity }) })
37
- const missing = (entity: string): KitFailure => new KitFailure('not_found', 'Skill not found', { entity })
36
+ new KitFailure('conflict', 'Skill conflict', {
37
+ reason,
38
+ ...(entity === undefined ? {} : { entity }),
39
+ })
40
+ const missing = (entity: string): KitFailure =>
41
+ new KitFailure('not_found', 'Skill not found', { entity })
38
42
  export function fileMetas(skill: SkillPackage): SkillFileMeta[] {
39
43
  return skill.files.map((file) => ({ path: file.path, kind: file.kind, size: file.bytes.length }))
40
44
  }
@@ -79,7 +83,12 @@ function pinned(catalog: SkillCatalog, id: string, expected: number): SkillRecor
79
83
  export function files(catalog: SkillCatalog, id: string, expected: number): SkillFileMeta[] {
80
84
  return fileMetas(pinned(catalog, id, expected))
81
85
  }
82
- export function file(catalog: SkillCatalog, id: string, expected: number, path: string): Uint8Array<ArrayBuffer> {
86
+ export function file(
87
+ catalog: SkillCatalog,
88
+ id: string,
89
+ expected: number,
90
+ path: string,
91
+ ): Uint8Array<ArrayBuffer> {
83
92
  const found = pinned(catalog, id, expected).files.find((entry) => entry.path === path)
84
93
  if (!found) throw missing('skill_file')
85
94
  return new Uint8Array(found.bytes)
@@ -97,8 +106,20 @@ function ensureFree(catalog: SkillCatalog, name: string, except?: string): void
97
106
  function mutable(skill: SkillRecord): void {
98
107
  if (skill.origin === 'builtin') throw badRequest('id', 'builtin')
99
108
  }
100
- async function create(content: SkillPackage, id: string, origin: SkillOrigin, now: number): Promise<SkillRecord> {
101
- return { ...content, id, origin, revision: 1, digest: await digest(content, content.files), updatedAt: now }
109
+ async function create(
110
+ content: SkillPackage,
111
+ id: string,
112
+ origin: SkillOrigin,
113
+ now: number,
114
+ ): Promise<SkillRecord> {
115
+ return {
116
+ ...content,
117
+ id,
118
+ origin,
119
+ revision: 1,
120
+ digest: await digest(content, content.files),
121
+ updatedAt: now,
122
+ }
102
123
  }
103
124
  async function update(
104
125
  previous: SkillRecord,
@@ -157,12 +178,20 @@ export async function save(
157
178
  newId: string,
158
179
  now: number,
159
180
  ): Promise<SkillCatalog> {
160
- const fields = { name: input.name, description: input.description, version: input.version ?? null, body: input.body }
181
+ const fields = {
182
+ name: input.name,
183
+ description: input.description,
184
+ version: input.version ?? null,
185
+ body: input.body,
186
+ }
161
187
  validateSkillContent(fields)
162
188
  if (input.id === undefined) {
163
189
  ensureFree(catalog, input.name)
164
190
  ensureId(catalog, newId)
165
- return [...catalog, await create({ ...fields, license: null, extra: {}, files: [] }, newId, 'manual', now)]
191
+ return [
192
+ ...catalog,
193
+ await create({ ...fields, license: null, extra: {}, files: [] }, newId, 'manual', now),
194
+ ]
166
195
  }
167
196
  if (input.expectedRevision === undefined) throw badRequest('expectedRevision', 'missing')
168
197
  const current = record(catalog, input.id)
@@ -231,7 +260,10 @@ export async function syncBuiltins(
231
260
  result = [...result, await create(content, id, 'builtin', now)]
232
261
  }
233
262
  }
234
- return result.length === catalog.length && result.every((skill, index) => skill === catalog[index]) ? catalog : result
263
+ return result.length === catalog.length &&
264
+ result.every((skill, index) => skill === catalog[index])
265
+ ? catalog
266
+ : result
235
267
  }
236
268
 
237
269
  /** Strictly narrows persisted documents before exposing their contents to services. */
@@ -260,16 +292,20 @@ export function parseCatalog(value: unknown): SkillCatalog {
260
292
  'skill',
261
293
  )
262
294
  const origin = entry.origin
263
- if (origin !== 'builtin' && origin !== 'manual' && origin !== 'imported') throw badRequest('origin', 'invalid')
295
+ if (origin !== 'builtin' && origin !== 'manual' && origin !== 'imported')
296
+ throw badRequest('origin', 'invalid')
264
297
  const extras = object(entry.extra, 'extra')
265
- const extra = Object.fromEntries(Object.entries(extras).map(([key, value]) => [key, text(value, 'extra')]))
298
+ const extra = Object.fromEntries(
299
+ Object.entries(extras).map(([key, value]) => [key, text(value, 'extra')]),
300
+ )
266
301
  const fileValues: unknown = entry.files
267
302
  if (!Array.isArray(fileValues)) throw badRequest('files', 'invalid')
268
303
  const parsedFiles: PackageFile[] = fileValues.map((rawFile: unknown) => {
269
304
  const file = object(rawFile, 'file')
270
305
  exact(file, ['path', 'kind', 'sha256', 'bytes'], 'file')
271
306
  const kind = file.kind
272
- if (kind !== 'script' && kind !== 'reference' && kind !== 'asset') throw badRequest('kind', 'invalid')
307
+ if (kind !== 'script' && kind !== 'reference' && kind !== 'asset')
308
+ throw badRequest('kind', 'invalid')
273
309
  const bytes: unknown = file.bytes
274
310
  if (!Array.isArray(bytes)) throw badRequest('bytes', 'invalid')
275
311
  const content = bytes.map((byte: unknown) => {
@@ -281,10 +317,15 @@ export function parseCatalog(value: unknown): SkillCatalog {
281
317
  if (!/^[0-9a-f]{64}$/u.test(hash)) throw badRequest('sha256', 'invalid')
282
318
  return { path: text(file.path, 'path'), kind, sha256: hash, bytes: content }
283
319
  })
284
- if (parsedFiles.some((file, index) => index > 0 && file.path <= (parsedFiles[index - 1]?.path ?? '')))
320
+ if (
321
+ parsedFiles.some(
322
+ (file, index) => index > 0 && file.path <= (parsedFiles[index - 1]?.path ?? ''),
323
+ )
324
+ )
285
325
  throw badRequest('files', 'invalid')
286
326
  const updatedAt = entry.updatedAt
287
- if (typeof updatedAt !== 'number' || !Number.isSafeInteger(updatedAt)) throw badRequest('updatedAt', 'invalid')
327
+ if (typeof updatedAt !== 'number' || !Number.isSafeInteger(updatedAt))
328
+ throw badRequest('updatedAt', 'invalid')
288
329
  const skill: SkillRecord = {
289
330
  id: text(entry.id, 'id'),
290
331
  name: text(entry.name, 'name'),
@@ -14,8 +14,13 @@ export function object(value: unknown, field: string): Record<string, unknown> {
14
14
  if (!isObject(value)) throw badRequest(field, 'invalid')
15
15
  return value
16
16
  }
17
- export function exact(value: Record<string, unknown>, keys: readonly string[], field: string): void {
18
- if (Object.keys(value).some((key) => !keys.includes(key))) throw badRequest(field, 'unknown_field')
17
+ export function exact(
18
+ value: Record<string, unknown>,
19
+ keys: readonly string[],
20
+ field: string,
21
+ ): void {
22
+ if (Object.keys(value).some((key) => !keys.includes(key)))
23
+ throw badRequest(field, 'unknown_field')
19
24
  }
20
25
  export function text(value: unknown, field: string): string {
21
26
  if (typeof value !== 'string') throw badRequest(field, 'invalid')
@@ -31,7 +36,17 @@ export function strings(value: unknown, field: string): string[] {
31
36
  return value.map((entry: unknown) => text(entry, field))
32
37
  }
33
38
  export function checkSecrets(value: string, field: string): void {
34
- const prefixes = ['sk-', 'ghp_', 'gho_', 'github_pat_', 'xoxb-', 'xoxp-', 'AKIA', 'AIza', 'glpat-']
39
+ const prefixes = [
40
+ 'sk-',
41
+ 'ghp_',
42
+ 'gho_',
43
+ 'github_pat_',
44
+ 'xoxb-',
45
+ 'xoxp-',
46
+ 'AKIA',
47
+ 'AIza',
48
+ 'glpat-',
49
+ ]
35
50
  let lineNumber = 0
36
51
  for (const line of value.split('\n')) {
37
52
  lineNumber++
@@ -40,17 +55,22 @@ export function checkSecrets(value: string, field: string): void {
40
55
  if (
41
56
  (line.includes('-----BEGIN') && line.includes('PRIVATE KEY')) ||
42
57
  (bearer?.[1] !== undefined && /^[A-Za-z0-9_./+=-]{20}/u.test(bearer[1])) ||
43
- tokens.some((token) => token.length >= 20 && prefixes.some((prefix) => token.startsWith(prefix)))
58
+ tokens.some(
59
+ (token) => token.length >= 20 && prefixes.some((prefix) => token.startsWith(prefix)),
60
+ )
44
61
  ) {
45
62
  throw badRequest(`${field}:${lineNumber}`, 'secret_detected')
46
63
  }
47
64
  }
48
65
  }
49
- export function validateSkillContent(skill: Pick<ContentIdentity, 'name' | 'description' | 'version' | 'body'>): void {
66
+ export function validateSkillContent(
67
+ skill: Pick<ContentIdentity, 'name' | 'description' | 'version' | 'body'>,
68
+ ): void {
50
69
  if (!/^[a-z0-9][a-z0-9-]{0,63}$/u.test(skill.name)) throw badRequest('name', 'invalid')
51
70
  if ([...skill.description].length < 1 || [...skill.description].length > 500)
52
71
  throw badRequest('description', 'invalid', 500)
53
- if (skill.version !== null && [...skill.version].length > 32) throw badRequest('version', 'invalid', 32)
72
+ if (skill.version !== null && [...skill.version].length > 32)
73
+ throw badRequest('version', 'invalid', 32)
54
74
  if (!skill.body.trim()) throw badRequest('body', 'empty')
55
75
  if (encoder.encode(skill.body).length > 65536) throw badRequest('body', 'too_large', 65536)
56
76
  checkSecrets(skill.name, 'name')
@@ -65,7 +85,8 @@ export function parseManualSave(value: unknown): ManualSave {
65
85
  name: text(input.name, 'name'),
66
86
  description: text(input.description, 'description'),
67
87
  body: text(input.body, 'body'),
68
- version: input.version === undefined || input.version === null ? null : text(input.version, 'version'),
88
+ version:
89
+ input.version === undefined || input.version === null ? null : text(input.version, 'version'),
69
90
  ...(input.id === undefined ? {} : { id: text(input.id, 'id') }),
70
91
  ...(input.expectedRevision === undefined
71
92
  ? {}
@@ -136,7 +157,8 @@ export function parseSkillMarkdown(raw: string): ParsedSkill {
136
157
  fields.set(key, value)
137
158
  }
138
159
  if (body === undefined) throw badRequest(String(lineNumber + 1), 'frontmatter_unsupported')
139
- if (body.split('\n', 1)[0] === '---') throw badRequest(String(lineNumber + 1), 'frontmatter_unsupported')
160
+ if (body.split('\n', 1)[0] === '---')
161
+ throw badRequest(String(lineNumber + 1), 'frontmatter_unsupported')
140
162
  const name = fields.get('name')
141
163
  const description = fields.get('description')
142
164
  if (name === undefined) throw badRequest('name', 'missing')
@@ -145,7 +167,14 @@ export function parseSkillMarkdown(raw: string): ParsedSkill {
145
167
  fields.delete('name')
146
168
  fields.delete('description')
147
169
  fields.delete('version')
148
- const skill: ParsedSkill = { name, description, version, body, license: null, extra: Object.fromEntries(fields) }
170
+ const skill: ParsedSkill = {
171
+ name,
172
+ description,
173
+ version,
174
+ body,
175
+ license: null,
176
+ extra: Object.fromEntries(fields),
177
+ }
149
178
  validateSkillContent(skill)
150
179
  return skill
151
180
  }