@linxin666/dsh-pet 0.2.4 → 0.2.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.
Files changed (44) 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 +109 -27
  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/image-dimensions.test.ts +85 -0
  40. package/src/image-dimensions.ts +76 -0
  41. package/src/index.ts +1 -1
  42. package/src/registry.test.ts +58 -5
  43. package/src/registry.ts +40 -1
  44. package/src/routes.ts +38 -34
@@ -32,6 +32,26 @@ const TAP_GROUP = 'TapBody';
32
32
  * downsampled atlas only when the effective on-screen scale warrants it.
33
33
  */
34
34
  const TEXTURE_OPTIONS = { lod: 'single-auto' };
35
+ /** Recursively release the activation without invalidating shared texture caches. */
36
+ const DESTROY_OPTIONS = { children: true };
37
+ /** Remove only this activation's canvas; `true` would release Pixi globals. */
38
+ const RENDERER_DESTROY_OPTIONS = { removeView: true };
39
+ /** Ignore hidden/zero boxes and keep Pixi dimensions stable and integral. */
40
+ function normalizeRendererSize(width, height) {
41
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0)
42
+ return undefined;
43
+ return {
44
+ width: Math.max(1, Math.round(width)),
45
+ height: Math.max(1, Math.round(height)),
46
+ };
47
+ }
48
+ /** Fit the model from its unscaled dimensions into the current Pixi screen. */
49
+ function layoutModel(app, model, sourceSize, config) {
50
+ const fit = Math.min(app.renderer.width / sourceSize.width, app.renderer.height / sourceSize.height) * 0.92;
51
+ model.scale.set(fit * (config.scale ?? 1));
52
+ model.anchor.set(0.5);
53
+ model.position.set(app.renderer.width / 2 + (config.translate?.x ?? 0), app.renderer.height / 2 + (config.translate?.y ?? 0));
54
+ }
35
55
  let vendorConfigured = false;
36
56
  /** Configure pixi extensions + the Cubism SDK once per page. */
37
57
  function configureOnce(vendor) {
@@ -67,11 +87,65 @@ export const live2dRenderer = {
67
87
  let disposed = false;
68
88
  let app;
69
89
  let model;
90
+ let modelAttached = false;
91
+ let modelSourceSize;
92
+ let resizeObserver;
93
+ let resizeTracking = false;
70
94
  let errorListener;
71
95
  let unsubscribe;
72
96
  /** The motion group the current phase maps to (resume target after taps). */
73
97
  let phaseGroup = config.motions.idle;
74
98
  let tapPlaying = false;
99
+ const stopResizeTracking = () => {
100
+ resizeTracking = false;
101
+ resizeObserver?.disconnect();
102
+ resizeObserver = undefined;
103
+ };
104
+ const resizeRenderer = (pixiApp, width, height) => {
105
+ const next = normalizeRendererSize(width, height);
106
+ if (disposed || !resizeTracking || next === undefined)
107
+ return;
108
+ if (pixiApp.renderer.width === next.width && pixiApp.renderer.height === next.height)
109
+ return;
110
+ pixiApp.renderer.resize(next.width, next.height);
111
+ if (model !== undefined && modelSourceSize !== undefined) {
112
+ layoutModel(pixiApp, model, modelSourceSize, config);
113
+ }
114
+ };
115
+ const trackContainerSize = (pixiApp) => {
116
+ if (typeof ResizeObserver === 'undefined')
117
+ return;
118
+ resizeTracking = true;
119
+ resizeObserver = new ResizeObserver((entries) => {
120
+ const entry = entries.find(candidate => candidate.target === ctx.container);
121
+ if (entry === undefined)
122
+ return;
123
+ resizeRenderer(pixiApp, entry.contentRect.width, entry.contentRect.height);
124
+ });
125
+ resizeObserver.observe(ctx.container);
126
+ // Catch a synchronous layout change between init() and observe().
127
+ resizeRenderer(pixiApp, ctx.container.clientWidth, ctx.container.clientHeight);
128
+ };
129
+ /** Release every resource currently owned by this activation exactly once. */
130
+ const destroyResources = () => {
131
+ stopResizeTracking();
132
+ unsubscribe?.();
133
+ unsubscribe = undefined;
134
+ const currentApp = app;
135
+ const currentModel = model;
136
+ const modelOwnedByApp = currentApp !== undefined && modelAttached;
137
+ app = undefined;
138
+ model = undefined;
139
+ modelSourceSize = undefined;
140
+ modelAttached = false;
141
+ try {
142
+ if (currentModel !== undefined && !modelOwnedByApp)
143
+ currentModel.destroy(DESTROY_OPTIONS);
144
+ }
145
+ finally {
146
+ currentApp?.destroy(RENDERER_DESTROY_OPTIONS, DESTROY_OPTIONS);
147
+ }
148
+ };
75
149
  const playGroup = (group) => {
76
150
  if (model === undefined)
77
151
  return;
@@ -107,40 +181,62 @@ export const live2dRenderer = {
107
181
  }
108
182
  configureOnce(vendor);
109
183
  const pixiApp = new vendor.Application();
110
- await pixiApp.init({
111
- width: Math.max(1, ctx.container.clientWidth || 160),
112
- height: Math.max(1, ctx.container.clientHeight || 174),
113
- backgroundAlpha: 0,
114
- antialias: true,
115
- autoDensity: true,
116
- preference: 'webgl',
117
- });
184
+ const initialSize = normalizeRendererSize(ctx.container.clientWidth, ctx.container.clientHeight) ?? {
185
+ width: 160,
186
+ height: 174,
187
+ };
188
+ try {
189
+ await pixiApp.init({
190
+ width: initialSize.width,
191
+ height: initialSize.height,
192
+ backgroundAlpha: 0,
193
+ antialias: true,
194
+ autoDensity: true,
195
+ preference: 'webgl',
196
+ });
197
+ }
198
+ catch (error) {
199
+ // init() can fail after allocating a partial renderer; cleanup is
200
+ // best-effort because Pixi may not consider that partial app ready.
201
+ try {
202
+ pixiApp.destroy(RENDERER_DESTROY_OPTIONS, DESTROY_OPTIONS);
203
+ }
204
+ catch { }
205
+ throw error;
206
+ }
118
207
  if (disposed) {
119
- pixiApp.destroy(true, { children: true });
208
+ pixiApp.destroy(RENDERER_DESTROY_OPTIONS, DESTROY_OPTIONS);
120
209
  return;
121
210
  }
211
+ app = pixiApp;
122
212
  pixiApp.canvas.style.display = 'block';
123
213
  pixiApp.canvas.style.width = '100%';
124
214
  pixiApp.canvas.style.height = '100%';
125
215
  ctx.container.appendChild(pixiApp.canvas);
216
+ // Keep a model that rejects during setup off Ticker.shared; from()
217
+ // does not expose that partial instance to callers for disposal.
218
+ trackContainerSize(pixiApp);
126
219
  const loaded = await vendor.Live2DModel.from(config.modelUrl, {
220
+ autoUpdate: false,
127
221
  autoHitTest: false,
128
222
  autoFocus: false,
129
223
  textureOptions: TEXTURE_OPTIONS,
130
224
  });
225
+ model = loaded;
131
226
  if (disposed) {
132
- pixiApp.destroy(true, { children: true });
227
+ destroyResources();
133
228
  return;
134
229
  }
135
- app = pixiApp;
136
- model = loaded;
230
+ modelSourceSize = {
231
+ width: Math.max(1, loaded.width),
232
+ height: Math.max(1, loaded.height),
233
+ };
137
234
  // Auto-fit the model into the container; the manifest scale multiplies
138
235
  // the fit and translate offsets from the center anchor.
139
- const fit = Math.min(pixiApp.renderer.width / Math.max(1, loaded.width), pixiApp.renderer.height / Math.max(1, loaded.height)) * 0.92;
140
- loaded.scale.set(fit * (config.scale ?? 1));
141
- loaded.anchor.set(0.5);
142
- loaded.position.set(pixiApp.renderer.width / 2 + (config.translate?.x ?? 0), pixiApp.renderer.height / 2 + (config.translate?.y ?? 0));
236
+ layoutModel(pixiApp, loaded, modelSourceSize, config);
143
237
  pixiApp.stage.addChild(loaded);
238
+ modelAttached = true;
239
+ loaded.automator.autoUpdate = true;
144
240
  // Resume the phase group once a tap motion finishes playing.
145
241
  loaded.on('motionFinish', () => {
146
242
  if (tapPlaying) {
@@ -152,20 +248,20 @@ export const live2dRenderer = {
152
248
  unsubscribe = ctx.phase.subscribe(applyPhase);
153
249
  };
154
250
  void boot().catch(() => {
155
- if (!disposed)
156
- errorListener?.('load-failed');
251
+ try {
252
+ destroyResources();
253
+ }
254
+ finally {
255
+ if (!disposed)
256
+ errorListener?.('load-failed');
257
+ }
157
258
  });
158
259
  return {
159
260
  dispose() {
160
261
  if (disposed)
161
262
  return;
162
263
  disposed = true;
163
- unsubscribe?.();
164
- model = undefined;
165
- if (app !== undefined) {
166
- app.destroy(true, { children: true });
167
- app = undefined;
168
- }
264
+ destroyResources();
169
265
  },
170
266
  tap(x, y) {
171
267
  const current = model;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Minimal PNG/WebP dimension reader — header-only, no decoding, no
3
+ * dependencies. Used by the decoration registry to verify a strip's actual
4
+ * pixel geometry matches its descriptor (single-row sprite strip; the client
5
+ * renders by frame-column offsets, so a mismatched strip silently shows the
6
+ * wrong frames). Parsing is best-effort: an unrecognized or truncated header
7
+ * returns undefined (the caller decides whether to warn).
8
+ *
9
+ * PNG: signature (8) + IHDR chunk — length (4) + 'IHDR' (4) + width (4) +
10
+ * height (4), both big-endian uint32 at fixed offsets 16/20.
11
+ * WebP: RIFF header (12) + chunk — 'VP8X' extended (width-1/height-1 as
12
+ * little-endian uint24 at 24/27), 'VP8L' lossless (packed 14-bit dims at
13
+ * 21), or 'VP8 ' lossy (frame header, low 14 bits of the uint16 at 26/28).
14
+ * @module @linxin666/dsh-pet/image-dimensions
15
+ */
16
+ export interface ImageDimensions {
17
+ width: number;
18
+ height: number;
19
+ }
20
+ /**
21
+ * Read image pixel dimensions from a PNG or WebP buffer. Returns undefined
22
+ * for formats this reader does not recognize (never throws). Callers treat
23
+ * undefined as "cannot verify", not as an error.
24
+ */
25
+ export declare function imageDimensions(buf: Buffer): ImageDimensions | undefined;
26
+ //# sourceMappingURL=image-dimensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-dimensions.d.ts","sourceRoot":"","sources":["../../src/image-dimensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AA8CD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAGxE"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Minimal PNG/WebP dimension reader — header-only, no decoding, no
3
+ * dependencies. Used by the decoration registry to verify a strip's actual
4
+ * pixel geometry matches its descriptor (single-row sprite strip; the client
5
+ * renders by frame-column offsets, so a mismatched strip silently shows the
6
+ * wrong frames). Parsing is best-effort: an unrecognized or truncated header
7
+ * returns undefined (the caller decides whether to warn).
8
+ *
9
+ * PNG: signature (8) + IHDR chunk — length (4) + 'IHDR' (4) + width (4) +
10
+ * height (4), both big-endian uint32 at fixed offsets 16/20.
11
+ * WebP: RIFF header (12) + chunk — 'VP8X' extended (width-1/height-1 as
12
+ * little-endian uint24 at 24/27), 'VP8L' lossless (packed 14-bit dims at
13
+ * 21), or 'VP8 ' lossy (frame header, low 14 bits of the uint16 at 26/28).
14
+ * @module @linxin666/dsh-pet/image-dimensions
15
+ */
16
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
17
+ /** Read the pixel size of a PNG buffer, or undefined when unrecognized. */
18
+ function pngDimensions(buf) {
19
+ if (buf.length < 24)
20
+ return undefined;
21
+ if (!buf.subarray(0, 8).equals(PNG_SIGNATURE))
22
+ return undefined;
23
+ if (buf.toString('ascii', 12, 16) !== 'IHDR')
24
+ return undefined;
25
+ return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
26
+ }
27
+ /** Read the pixel size of a WebP buffer, or undefined when unrecognized. */
28
+ function webpDimensions(buf) {
29
+ if (buf.length < 21)
30
+ return undefined;
31
+ if (buf.toString('ascii', 0, 4) !== 'RIFF')
32
+ return undefined;
33
+ if (buf.toString('ascii', 8, 12) !== 'WEBP')
34
+ return undefined;
35
+ const fourcc = buf.toString('ascii', 12, 16);
36
+ if (fourcc === 'VP8X') {
37
+ // Extended header: 1-byte flags at 20, then width-1/height-1 uint24 LE.
38
+ if (buf.length < 30)
39
+ return undefined;
40
+ return {
41
+ width: 1 + buf.readUIntLE(24, 3),
42
+ height: 1 + buf.readUIntLE(27, 3),
43
+ };
44
+ }
45
+ if (fourcc === 'VP8L') {
46
+ // Lossless header: 0x2f marker at 20, then 14-bit width / 14-bit height
47
+ // packed into the little-endian uint32 at 21.
48
+ if (buf.length < 25)
49
+ return undefined;
50
+ const bits = buf.readUInt32LE(21);
51
+ return {
52
+ width: 1 + (bits & 0x3fff),
53
+ height: 1 + ((bits >>> 14) & 0x3fff),
54
+ };
55
+ }
56
+ if (fourcc === 'VP8 ') {
57
+ // Lossy frame header: 3-byte tag + 3-byte start code, then width/height
58
+ // as uint16 LE whose low 14 bits carry the dimension.
59
+ if (buf.length < 30)
60
+ return undefined;
61
+ return {
62
+ width: buf.readUInt16LE(26) & 0x3fff,
63
+ height: buf.readUInt16LE(28) & 0x3fff,
64
+ };
65
+ }
66
+ return undefined;
67
+ }
68
+ /**
69
+ * Read image pixel dimensions from a PNG or WebP buffer. Returns undefined
70
+ * for formats this reader does not recognize (never throws). Callers treat
71
+ * undefined as "cannot verify", not as an error.
72
+ */
73
+ export function imageDimensions(buf) {
74
+ if (buf.length >= 12 && buf.toString('ascii', 0, 4) === 'RIFF')
75
+ return webpDimensions(buf);
76
+ return pngDimensions(buf);
77
+ }
@@ -80,7 +80,7 @@ function applyImpl(ctx, config = {}) {
80
80
  // pattern as dsh-remote-web-ui's /api/pair family). The routes are
81
81
  // registered while the plugin is enabled; toggling the setting off makes
82
82
  // the pet API disappear until it is re-enabled.
83
- const routes = makePetRoutes({ service });
83
+ const routes = makePetRoutes({ service, ctx });
84
84
  let disposeRoutes;
85
85
  const syncRoutes = () => {
86
86
  const enabled = current().enabled ?? true;
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/registry.ts"],"names":[],"mappings":"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"}
@@ -26,13 +26,14 @@
26
26
  * whale-girl manifest overrides its own durations.
27
27
  * @module @linxin666/dsh-pet/registry
28
28
  */
29
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
29
+ import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
30
30
  import { homedir } from 'node:os';
31
31
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
32
32
  import { fileURLToPath } from 'node:url';
33
33
  import { normalizePetRemarks } from './remarks.js';
34
34
  import { mergeVoicePacks, normalizeVoicePack } from './voice-pack.js';
35
35
  import { parseDecorationManifest } from './decoration.js';
36
+ import { imageDimensions } from './image-dimensions.js';
36
37
  import { PET_DECORATION_API_VERSION } from './contracts/status-decoration.js';
37
38
  import { dshHome } from './dsh-home.js';
38
39
  import { parsePetManifest } from './manifest-v2.js';
@@ -473,6 +474,29 @@ function loadVoicePackFile(file, options) {
473
474
  }
474
475
  /** Decoration asset URL prefix (served by the decoration route, M5). */
475
476
  export const DECORATION_ASSET_PREFIX = '/api/pet/decoration';
477
+ /** Read the pixel dimensions of a decoration strip (PNG/WebP), if decodable. */
478
+ function readImageDimensions(file) {
479
+ let header;
480
+ try {
481
+ // Only the header is needed for dimensions; cap the read so a huge or
482
+ // corrupt strip cannot balloon memory during the registry scan.
483
+ const fd = openSync(file, 'r');
484
+ try {
485
+ header = Buffer.alloc(64);
486
+ const read = readSync(fd, header, 0, header.length, 0);
487
+ if (read < 0)
488
+ return undefined;
489
+ header = header.subarray(0, read);
490
+ }
491
+ finally {
492
+ closeSync(fd);
493
+ }
494
+ }
495
+ catch {
496
+ return undefined;
497
+ }
498
+ return imageDimensions(header);
499
+ }
476
500
  /**
477
501
  * Scan one directory of decoration folders ('decoration.json' + strip).
478
502
  * Later scans override earlier ones on id collision; a bad descriptor warns
@@ -523,6 +547,24 @@ function scanDecorationDir(dir, options) {
523
547
  options.warnings?.push(message);
524
548
  options.diagnostics?.push({ level: 'warning', source: entryDir, message });
525
549
  }
550
+ else {
551
+ // Geometry check: the client renders the strip as a single row of
552
+ // 'columns' frames (background-position advances by frame width only),
553
+ // so the strip must be exactly cell.width * columns wide and cell.height
554
+ // tall. A mismatched strip silently shows the wrong/partial frames —
555
+ // warn-and-keep, mirroring the missing-strip discipline (never throw).
556
+ const actual = readImageDimensions(join(entryDir, manifest.entry));
557
+ if (actual !== undefined) {
558
+ const expectedWidth = manifest.cell.width * manifest.columns;
559
+ if (actual.width !== expectedWidth || actual.height !== manifest.cell.height) {
560
+ const message = 'decoration ' + manifest.id + ': strip ' + actual.width + 'x' + actual.height
561
+ + ' does not match cell ' + manifest.cell.width + 'x' + manifest.cell.height + ' x ' + manifest.columns
562
+ + ' columns (expected ' + expectedWidth + 'x' + manifest.cell.height + '); frames will render wrong';
563
+ options.warnings?.push(message);
564
+ options.diagnostics?.push({ level: 'warning', source: entryDir, message });
565
+ }
566
+ }
567
+ }
526
568
  entries.push({
527
569
  apiVersion: PET_DECORATION_API_VERSION,
528
570
  id: manifest.id,
@@ -5,9 +5,13 @@
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
  */
14
+ import type { Context } from '@deepseek-ai/cordis';
11
15
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
12
16
  import type { PetService } from './service.ts';
13
17
  /** Browser-facing base path of the pet API. */
@@ -53,6 +57,7 @@ export interface PetRuntimeRoots {
53
57
  /** Build the full route family (API + assets + runtime) for one service. */
54
58
  export declare function makePetRoutes(deps: {
55
59
  service: PetService;
60
+ ctx: Context;
56
61
  assetCaps?: PetAssetCaps;
57
62
  } & PetRuntimeRoots): WebRoute[];
58
63
  export { petPackageRoot } from './registry.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAM9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;aACzB,QAAQ;IACR,iDAAiD;aACjD,KAAK;IACL,8EAA8E;aAC9E,KAAK;CACG,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAWD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUrF;AAsPD,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA8B,CAAA;AAe7D,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwMD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CAwDnH;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAM9C,+CAA+C;AAC/C,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,0EAA0E;AAC1E,eAAO,MAAM,gBAAgB,SAAS,CAAA;AAMtC;;;;GAIG;AACH,eAAO,MAAM,cAAc;IACzB,yBAAyB;aACzB,QAAQ;IACR,iDAAiD;aACjD,KAAK;IACL,8EAA8E;aAC9E,KAAK;CACG,CAAA;AAEV,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAWD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUrF;AAsPD,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA8B,CAAA;AAe7D,4EAA4E;AAC5E,eAAO,MAAM,eAAe,QAAmB,CAAA;AAE/C,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAwMD,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,GAAG,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,YAAY,CAAA;CAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CAwDjI;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA"}
@@ -5,14 +5,17 @@
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
  import { existsSync, realpathSync, statSync } from 'node:fs';
12
15
  import { readFile } from 'node:fs/promises';
13
16
  import { join, sep } from 'node:path';
14
17
  import { DECORATION_ASSET_PREFIX, petEntryView, petPackageRoot } from './registry.js';
15
- import { isLoopbackRequest } from './loopback.js';
18
+ import { isPetAllowed } from './access.js';
16
19
  import { dshHome } from './dsh-home.js';
17
20
  /** Browser-facing base path of the pet API. */
18
21
  export const PET_API_PREFIX = '/api/pet';
@@ -114,20 +117,20 @@ function readJsonBody(req) {
114
117
  req.on('error', reject);
115
118
  });
116
119
  }
117
- /** Shared route fence: the browser UI is a loopback client; LAN hosts stay out. */
118
- function guard(req, res) {
119
- if (isLoopbackRequest(req))
120
+ /** Shared route fence: loopback always passes; a live paired-device cookie is an extra allow path. */
121
+ function guard(ctx, req, res) {
122
+ if (isPetAllowed(ctx, req))
120
123
  return true;
121
124
  json(res, 403, { ok: false, error: 'forbidden: loopback-only' });
122
125
  return false;
123
126
  }
124
127
  /** Wrap one async service call as a GET JSON route. */
125
- function getRoute(path, run) {
128
+ function getRoute(ctx, path, run) {
126
129
  return {
127
130
  kind: 'exact',
128
131
  path,
129
132
  handler: (req, res) => {
130
- if (!guard(req, res))
133
+ if (!guard(ctx, req, res))
131
134
  return;
132
135
  if (!requireMethod(req, res, 'GET'))
133
136
  return;
@@ -138,12 +141,12 @@ function getRoute(path, run) {
138
141
  };
139
142
  }
140
143
  /** Wrap one async service call as a POST JSON route (body passed through). */
141
- function postRoute(path, run) {
144
+ function postRoute(ctx, path, run) {
142
145
  return {
143
146
  kind: 'exact',
144
147
  path,
145
148
  handler: (req, res) => {
146
- if (!guard(req, res))
149
+ if (!guard(ctx, req, res))
147
150
  return Promise.resolve();
148
151
  if (!requireMethod(req, res, 'POST'))
149
152
  return Promise.resolve();
@@ -178,10 +181,10 @@ function dirAliases(registry) {
178
181
  * match; containedRealpath stays as the second layer. Composed pets without
179
182
  * a manifest file get a synthesized pet.json.
180
183
  */
181
- function assetHandler(registry, caps) {
184
+ function assetHandler(ctx, registry, caps) {
182
185
  const aliases = dirAliases(registry);
183
- return (req, res) => {
184
- if (!guard(req, res))
186
+ return ((req, res) => {
187
+ if (!guard(ctx, req, res))
185
188
  return;
186
189
  if (req.method !== 'GET' && req.method !== 'HEAD') {
187
190
  res.writeHead(405);
@@ -289,7 +292,7 @@ function assetHandler(registry, caps) {
289
292
  res.end();
290
293
  return;
291
294
  }
292
- readFile(resolved).then((body) => {
295
+ return readFile(resolved).then((body) => {
293
296
  res.writeHead(200, {
294
297
  'content-type': mimeFor(resolved),
295
298
  'content-length': String(body.byteLength),
@@ -304,7 +307,7 @@ function assetHandler(registry, caps) {
304
307
  res.writeHead(404);
305
308
  res.end();
306
309
  });
307
- };
310
+ });
308
311
  }
309
312
  /** Browser-facing base path of the plugin runtime files (pet-center M3). */
310
313
  export const PET_RUNTIME_PREFIX = PET_API_PREFIX + '/runtime';
@@ -328,9 +331,9 @@ export const PET_RUNTIME_CAP = 16 * 1024 * 1024;
328
331
  * guidance (the Cubism Core is user-supplied, so its absence is a normal
329
332
  * state, not an error).
330
333
  */
331
- function runtimeHandler(roots) {
332
- return (req, res) => {
333
- if (!guard(req, res))
334
+ function runtimeHandler(ctx, roots) {
335
+ return ((req, res) => {
336
+ if (!guard(ctx, req, res))
334
337
  return;
335
338
  if (req.method !== 'GET' && req.method !== 'HEAD') {
336
339
  res.writeHead(405);
@@ -387,7 +390,7 @@ function runtimeHandler(roots) {
387
390
  res.end();
388
391
  return;
389
392
  }
390
- readFile(resolved).then((body) => {
393
+ return readFile(resolved).then((body) => {
391
394
  res.writeHead(200, {
392
395
  'content-type': name.endsWith('.map') ? 'application/json' : 'application/javascript; charset=utf-8',
393
396
  'content-length': String(body.byteLength),
@@ -402,7 +405,7 @@ function runtimeHandler(roots) {
402
405
  res.writeHead(404);
403
406
  res.end();
404
407
  });
405
- };
408
+ });
406
409
  }
407
410
  /**
408
411
  * The decoration asset handler behind '/api/pet/decoration/<id>/<file>'
@@ -411,9 +414,9 @@ function runtimeHandler(roots) {
411
414
  * match, with realpath containment and the same size ceilings as pet
412
415
  * assets. Crafted '..' or '.' segments never match the normalized closure.
413
416
  */
414
- function decorationHandler(registry, caps) {
417
+ function decorationHandler(ctx, registry, caps) {
415
418
  return (req, res) => {
416
- if (!guard(req, res))
419
+ if (!guard(ctx, req, res))
417
420
  return;
418
421
  if (req.method !== 'GET' && req.method !== 'HEAD') {
419
422
  res.writeHead(405);
@@ -529,36 +532,36 @@ function decorationHandler(registry, caps) {
529
532
  }
530
533
  /** Build the full route family (API + assets + runtime) for one service. */
531
534
  export function makePetRoutes(deps) {
532
- const { service } = deps;
535
+ const { service, ctx } = deps;
533
536
  const apiRoutes = [
534
- getRoute(PET_API_PREFIX + '/state', () => service.state()),
535
- getRoute(PET_API_PREFIX + '/pets', () => service.pets()),
536
- getRoute(PET_API_PREFIX + '/diagnostics', () => service.diagnostics()),
537
- postRoute(PET_API_PREFIX + '/interact', (body) => {
537
+ getRoute(ctx, PET_API_PREFIX + '/state', () => service.state()),
538
+ getRoute(ctx, PET_API_PREFIX + '/pets', () => service.pets()),
539
+ getRoute(ctx, PET_API_PREFIX + '/diagnostics', () => service.diagnostics()),
540
+ postRoute(ctx, PET_API_PREFIX + '/interact', (body) => {
538
541
  const kind = body.kind;
539
542
  if (kind !== 'pet' && kind !== 'feed')
540
543
  return Promise.reject(new Error('invalid-kind'));
541
544
  return service.interact(kind);
542
545
  }),
543
- postRoute(PET_API_PREFIX + '/set-visible', (body) => {
546
+ postRoute(ctx, PET_API_PREFIX + '/set-visible', (body) => {
544
547
  const visible = body.visible;
545
548
  if (typeof visible !== 'boolean')
546
549
  return Promise.reject(new Error('invalid-visible'));
547
550
  return service.setVisible(visible);
548
551
  }),
549
- postRoute(PET_API_PREFIX + '/set-config', (body) => service.setConfig({
552
+ postRoute(ctx, PET_API_PREFIX + '/set-config', (body) => service.setConfig({
550
553
  ...(typeof body.size === 'number' ? { size: body.size } : {}),
551
554
  ...(typeof body.right === 'number' ? { right: body.right } : {}),
552
555
  ...(typeof body.bottom === 'number' ? { bottom: body.bottom } : {}),
553
556
  ...(typeof body.visible === 'boolean' ? { visible: body.visible } : {}),
554
557
  })),
555
- postRoute(PET_API_PREFIX + '/set-name', (body) => {
558
+ postRoute(ctx, PET_API_PREFIX + '/set-name', (body) => {
556
559
  const name = body.name;
557
560
  if (typeof name !== 'string')
558
561
  return Promise.reject(new Error('invalid-name'));
559
562
  return service.setName(name);
560
563
  }),
561
- postRoute(PET_API_PREFIX + '/set-pet', (body) => {
564
+ postRoute(ctx, PET_API_PREFIX + '/set-pet', (body) => {
562
565
  const petId = body.petId;
563
566
  if (typeof petId !== 'string')
564
567
  return Promise.reject(new Error('invalid-pet'));
@@ -568,12 +571,12 @@ export function makePetRoutes(deps) {
568
571
  const assetRoute = {
569
572
  kind: 'prefix',
570
573
  path: PET_ASSET_PREFIX,
571
- handler: assetHandler(service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
574
+ handler: assetHandler(ctx, service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
572
575
  };
573
576
  const runtimeRoute = {
574
577
  kind: 'prefix',
575
578
  path: PET_RUNTIME_PREFIX,
576
- handler: runtimeHandler({
579
+ handler: runtimeHandler(ctx, {
577
580
  runtimeDir: deps.runtimeDir ?? join(dshHome(), 'pets', '.runtime'),
578
581
  vendorDir: deps.vendorDir ?? join(petPackageRoot(import.meta.url), 'lib'),
579
582
  }),
@@ -581,7 +584,7 @@ export function makePetRoutes(deps) {
581
584
  const decorationRoute = {
582
585
  kind: 'prefix',
583
586
  path: DECORATION_ASSET_PREFIX,
584
- handler: decorationHandler(service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
587
+ handler: decorationHandler(ctx, service.registrySnapshot(), deps.assetCaps ?? PET_ASSET_CAPS),
585
588
  };
586
589
  return [...apiRoutes, assetRoute, runtimeRoute, decorationRoute];
587
590
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-pet",
3
3
  "description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
4
- "version": "0.2.4",
4
+ "version": "0.2.5",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"