@linxin666/dsh-pet 0.2.4 → 0.2.6

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 (45) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +1 -1
  3. package/README.zh.md +1 -1
  4. package/lib/client.js +110 -28
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +164 -34
  7. package/lib/types/access.d.ts +16 -0
  8. package/lib/types/access.d.ts.map +1 -0
  9. package/lib/types/access.js +20 -0
  10. package/lib/types/client/PetSettingsCard.d.ts.map +1 -1
  11. package/lib/types/client/PetSettingsCard.js +8 -2
  12. package/lib/types/client/PetSprite.d.ts.map +1 -1
  13. package/lib/types/client/PetSprite.js +10 -2
  14. package/lib/types/client/renderers/live2d/Live2dVisualMount.d.ts.map +1 -1
  15. package/lib/types/client/renderers/live2d/Live2dVisualMount.js +1 -0
  16. package/lib/types/client/renderers/live2d/runtime.d.ts +13 -1
  17. package/lib/types/client/renderers/live2d/runtime.d.ts.map +1 -1
  18. package/lib/types/client/renderers/live2d.d.ts.map +1 -1
  19. package/lib/types/client/renderers/live2d.js +120 -24
  20. package/lib/types/image-dimensions.d.ts +26 -0
  21. package/lib/types/image-dimensions.d.ts.map +1 -0
  22. package/lib/types/image-dimensions.js +77 -0
  23. package/lib/types/index.js +1 -1
  24. package/lib/types/registry.d.ts.map +1 -1
  25. package/lib/types/registry.js +43 -1
  26. package/lib/types/routes.d.ts +6 -1
  27. package/lib/types/routes.d.ts.map +1 -1
  28. package/lib/types/routes.js +36 -33
  29. package/package.json +1 -1
  30. package/src/access.ts +40 -0
  31. package/src/client/PetSettingsCard.tsx +8 -2
  32. package/src/client/PetSprite.test.tsx +71 -0
  33. package/src/client/PetSprite.tsx +10 -2
  34. package/src/client/renderers/live2d/Live2dVisualMount.test.tsx +72 -0
  35. package/src/client/renderers/live2d/Live2dVisualMount.tsx +1 -0
  36. package/src/client/renderers/live2d/runtime.ts +8 -2
  37. package/src/client/renderers/live2d.test.ts +211 -8
  38. package/src/client/renderers/live2d.ts +129 -29
  39. package/src/client/settings-card.module.css +2 -0
  40. package/src/image-dimensions.test.ts +85 -0
  41. package/src/image-dimensions.ts +76 -0
  42. package/src/index.ts +1 -1
  43. package/src/registry.test.ts +58 -5
  44. package/src/registry.ts +40 -1
  45. package/src/routes.ts +38 -34
package/src/routes.ts CHANGED
@@ -5,7 +5,10 @@
5
5
  * domains are platform-registered, so the pet serves its own API and media —
6
6
  * the same pattern as dsh-remote-web-ui's '/api/pair' family. The asset route
7
7
  * is one prefix registration serving every registry entry (manifest, atlas,
8
- * optional previews), so adding a pet never touches route wiring.
8
+ * optional previews), so adding a pet never touches route wiring. Both the
9
+ * JSON API, the asset prefix, and the Live2D runtime prefix are loopback-only
10
+ * by default; a live paired-device cookie is an extra allow path when
11
+ * remote-web-ui is loaded.
9
12
  * @module @linxin666/dsh-pet/routes
10
13
  */
11
14
 
@@ -13,11 +16,12 @@ import { existsSync, realpathSync, statSync } from 'node:fs'
13
16
  import { readFile } from 'node:fs/promises'
14
17
  import { join, sep } from 'node:path'
15
18
  import type { IncomingMessage, ServerResponse } from 'node:http'
19
+ import type { Context } from '@deepseek-ai/cordis'
16
20
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
17
21
  import type { PetService } from './service.ts'
18
22
  import type { PetInteraction } from './affinity.ts'
19
23
  import { DECORATION_ASSET_PREFIX, petEntryView, petPackageRoot, type PetEntry, type PetRegistry } from './registry.ts'
20
- import { isLoopbackRequest } from './loopback.ts'
24
+ import { isPetAllowed } from './access.ts'
21
25
  import { dshHome } from './dsh-home.ts'
22
26
 
23
27
  /** Browser-facing base path of the pet API. */
@@ -135,20 +139,20 @@ function readJsonBody(req: IncomingMessage): Promise<unknown> {
135
139
  })
136
140
  }
137
141
 
138
- /** Shared route fence: the browser UI is a loopback client; LAN hosts stay out. */
139
- function guard(req: IncomingMessage, res: ServerResponse): boolean {
140
- if (isLoopbackRequest(req)) return true
142
+ /** Shared route fence: loopback always passes; a live paired-device cookie is an extra allow path. */
143
+ function guard(ctx: Context, req: IncomingMessage, res: ServerResponse): boolean {
144
+ if (isPetAllowed(ctx, req)) return true
141
145
  json(res, 403, { ok: false, error: 'forbidden: loopback-only' })
142
146
  return false
143
147
  }
144
148
 
145
149
  /** Wrap one async service call as a GET JSON route. */
146
- function getRoute(path: string, run: () => Promise<unknown>): WebRoute {
150
+ function getRoute(ctx: Context, path: string, run: () => Promise<unknown>): WebRoute {
147
151
  return {
148
152
  kind: 'exact',
149
153
  path,
150
154
  handler: (req: IncomingMessage, res: ServerResponse): void => {
151
- if (!guard(req, res)) return
155
+ if (!guard(ctx, req, res)) return
152
156
  if (!requireMethod(req, res, 'GET')) return
153
157
  run().then((value) => json(res, 200, value), (error) => {
154
158
  json(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) })
@@ -158,12 +162,12 @@ function getRoute(path: string, run: () => Promise<unknown>): WebRoute {
158
162
  }
159
163
 
160
164
  /** Wrap one async service call as a POST JSON route (body passed through). */
161
- function postRoute(path: string, run: (body: Record<string, unknown>) => Promise<unknown>): WebRoute {
165
+ function postRoute(ctx: Context, path: string, run: (body: Record<string, unknown>) => Promise<unknown>): WebRoute {
162
166
  return {
163
167
  kind: 'exact',
164
168
  path,
165
169
  handler: (req: IncomingMessage, res: ServerResponse): Promise<void> => {
166
- if (!guard(req, res)) return Promise.resolve()
170
+ if (!guard(ctx, req, res)) return Promise.resolve()
167
171
  if (!requireMethod(req, res, 'POST')) return Promise.resolve()
168
172
  return readJsonBody(req).then((body) => {
169
173
  const record = (typeof body === 'object' && body !== null) ? body as Record<string, unknown> : {}
@@ -200,10 +204,10 @@ function dirAliases(registry: PetRegistry): Map<string, PetEntry> {
200
204
  * match; containedRealpath stays as the second layer. Composed pets without
201
205
  * a manifest file get a synthesized pet.json.
202
206
  */
203
- function assetHandler(registry: PetRegistry, caps: PetAssetCaps): WebRoute['handler'] {
207
+ function assetHandler(ctx: Context, registry: PetRegistry, caps: PetAssetCaps): WebRoute['handler'] {
204
208
  const aliases = dirAliases(registry)
205
- return (req: IncomingMessage, res: ServerResponse): void => {
206
- if (!guard(req, res)) return
209
+ return ((req: IncomingMessage, res: ServerResponse) => {
210
+ if (!guard(ctx, req, res)) return
207
211
  if (req.method !== 'GET' && req.method !== 'HEAD') {
208
212
  res.writeHead(405)
209
213
  res.end()
@@ -303,7 +307,7 @@ function assetHandler(registry: PetRegistry, caps: PetAssetCaps): WebRoute['hand
303
307
  res.end()
304
308
  return
305
309
  }
306
- readFile(resolved).then((body) => {
310
+ return readFile(resolved).then((body) => {
307
311
  res.writeHead(200, {
308
312
  'content-type': mimeFor(resolved),
309
313
  'content-length': String(body.byteLength),
@@ -318,7 +322,7 @@ function assetHandler(registry: PetRegistry, caps: PetAssetCaps): WebRoute['hand
318
322
  res.writeHead(404)
319
323
  res.end()
320
324
  })
321
- }
325
+ }) as WebRoute['handler']
322
326
  }
323
327
 
324
328
  /** Browser-facing base path of the plugin runtime files (pet-center M3). */
@@ -354,9 +358,9 @@ export interface PetRuntimeRoots {
354
358
  * guidance (the Cubism Core is user-supplied, so its absence is a normal
355
359
  * state, not an error).
356
360
  */
357
- function runtimeHandler(roots: { runtimeDir: string; vendorDir: string }): WebRoute['handler'] {
358
- return (req: IncomingMessage, res: ServerResponse): void => {
359
- if (!guard(req, res)) return
361
+ function runtimeHandler(ctx: Context, roots: { runtimeDir: string; vendorDir: string }): WebRoute['handler'] {
362
+ return ((req: IncomingMessage, res: ServerResponse) => {
363
+ if (!guard(ctx, req, res)) return
360
364
  if (req.method !== 'GET' && req.method !== 'HEAD') {
361
365
  res.writeHead(405)
362
366
  res.end()
@@ -409,7 +413,7 @@ function runtimeHandler(roots: { runtimeDir: string; vendorDir: string }): WebRo
409
413
  res.end()
410
414
  return
411
415
  }
412
- readFile(resolved).then((body) => {
416
+ return readFile(resolved).then((body) => {
413
417
  res.writeHead(200, {
414
418
  'content-type': name.endsWith('.map') ? 'application/json' : 'application/javascript; charset=utf-8',
415
419
  'content-length': String(body.byteLength),
@@ -424,7 +428,7 @@ function runtimeHandler(roots: { runtimeDir: string; vendorDir: string }): WebRo
424
428
  res.writeHead(404)
425
429
  res.end()
426
430
  })
427
- }
431
+ }) as WebRoute['handler']
428
432
  }
429
433
 
430
434
  /**
@@ -434,9 +438,9 @@ function runtimeHandler(roots: { runtimeDir: string; vendorDir: string }): WebRo
434
438
  * match, with realpath containment and the same size ceilings as pet
435
439
  * assets. Crafted '..' or '.' segments never match the normalized closure.
436
440
  */
437
- function decorationHandler(registry: PetRegistry, caps: PetAssetCaps): WebRoute['handler'] {
441
+ function decorationHandler(ctx: Context, registry: PetRegistry, caps: PetAssetCaps): WebRoute['handler'] {
438
442
  return (req: IncomingMessage, res: ServerResponse): void => {
439
- if (!guard(req, res)) return
443
+ if (!guard(ctx, req, res)) return
440
444
  if (req.method !== 'GET' && req.method !== 'HEAD') {
441
445
  res.writeHead(405)
442
446
  res.end()
@@ -547,34 +551,34 @@ function decorationHandler(registry: PetRegistry, caps: PetAssetCaps): WebRoute[
547
551
  }
548
552
 
549
553
  /** Build the full route family (API + assets + runtime) for one service. */
550
- export function makePetRoutes(deps: { service: PetService; assetCaps?: PetAssetCaps } & PetRuntimeRoots): WebRoute[] {
551
- const { service } = deps
554
+ export function makePetRoutes(deps: { service: PetService; ctx: Context; assetCaps?: PetAssetCaps } & PetRuntimeRoots): WebRoute[] {
555
+ const { service, ctx } = deps
552
556
  const apiRoutes: WebRoute[] = [
553
- getRoute(PET_API_PREFIX + '/state', () => service.state()),
554
- getRoute(PET_API_PREFIX + '/pets', () => service.pets()),
555
- getRoute(PET_API_PREFIX + '/diagnostics', () => service.diagnostics()),
556
- postRoute(PET_API_PREFIX + '/interact', (body) => {
557
+ getRoute(ctx, PET_API_PREFIX + '/state', () => service.state()),
558
+ getRoute(ctx, PET_API_PREFIX + '/pets', () => service.pets()),
559
+ getRoute(ctx, PET_API_PREFIX + '/diagnostics', () => service.diagnostics()),
560
+ postRoute(ctx, PET_API_PREFIX + '/interact', (body) => {
557
561
  const kind = body.kind as PetInteraction | undefined
558
562
  if (kind !== 'pet' && kind !== 'feed') return Promise.reject(new Error('invalid-kind'))
559
563
  return service.interact(kind)
560
564
  }),
561
- postRoute(PET_API_PREFIX + '/set-visible', (body) => {
565
+ postRoute(ctx, PET_API_PREFIX + '/set-visible', (body) => {
562
566
  const visible = body.visible
563
567
  if (typeof visible !== 'boolean') return Promise.reject(new Error('invalid-visible'))
564
568
  return service.setVisible(visible)
565
569
  }),
566
- postRoute(PET_API_PREFIX + '/set-config', (body) => service.setConfig({
570
+ postRoute(ctx, PET_API_PREFIX + '/set-config', (body) => service.setConfig({
567
571
  ...(typeof body.size === 'number' ? { size: body.size } : {}),
568
572
  ...(typeof body.right === 'number' ? { right: body.right } : {}),
569
573
  ...(typeof body.bottom === 'number' ? { bottom: body.bottom } : {}),
570
574
  ...(typeof body.visible === 'boolean' ? { visible: body.visible } : {}),
571
575
  })),
572
- postRoute(PET_API_PREFIX + '/set-name', (body) => {
576
+ postRoute(ctx, PET_API_PREFIX + '/set-name', (body) => {
573
577
  const name = body.name
574
578
  if (typeof name !== 'string') return Promise.reject(new Error('invalid-name'))
575
579
  return service.setName(name)
576
580
  }),
577
- postRoute(PET_API_PREFIX + '/set-pet', (body) => {
581
+ postRoute(ctx, PET_API_PREFIX + '/set-pet', (body) => {
578
582
  const petId = body.petId
579
583
  if (typeof petId !== 'string') return Promise.reject(new Error('invalid-pet'))
580
584
  return service.setPetId(petId)
@@ -584,13 +588,13 @@ export function makePetRoutes(deps: { service: PetService; assetCaps?: PetAssetC
584
588
  const assetRoute: WebRoute = {
585
589
  kind: 'prefix',
586
590
  path: PET_ASSET_PREFIX,
587
- handler: assetHandler(service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
591
+ handler: assetHandler(ctx, service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
588
592
  }
589
593
 
590
594
  const runtimeRoute: WebRoute = {
591
595
  kind: 'prefix',
592
596
  path: PET_RUNTIME_PREFIX,
593
- handler: runtimeHandler({
597
+ handler: runtimeHandler(ctx, {
594
598
  runtimeDir: deps.runtimeDir ?? join(dshHome(), 'pets', '.runtime'),
595
599
  vendorDir: deps.vendorDir ?? join(petPackageRoot(import.meta.url), 'lib'),
596
600
  }),
@@ -599,7 +603,7 @@ export function makePetRoutes(deps: { service: PetService; assetCaps?: PetAssetC
599
603
  const decorationRoute: WebRoute = {
600
604
  kind: 'prefix',
601
605
  path: DECORATION_ASSET_PREFIX,
602
- handler: decorationHandler(service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
606
+ handler: decorationHandler(ctx, service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
603
607
  }
604
608
 
605
609
  return [...apiRoutes, assetRoute, runtimeRoute, decorationRoute]