editmamei 0.17.5 → 0.19.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 (72) hide show
  1. package/NOTICES.md +43 -0
  2. package/README.md +91 -8
  3. package/dist/api/extendscript/_helpers.js +8 -0
  4. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  5. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  6. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  7. package/dist/cli/clients/json-config.js +1 -1
  8. package/dist/cli/config.js +7 -0
  9. package/dist/core/server.js +124 -29
  10. package/dist/core/settings.js +8 -0
  11. package/dist/core/tool-groups.js +178 -0
  12. package/dist/core/tool-tiers.js +34 -48
  13. package/dist/delivery/bundle.js +8 -0
  14. package/dist/delivery/config.js +23 -0
  15. package/dist/delivery/provision.js +30 -2
  16. package/dist/delivery/store.js +40 -1
  17. package/dist/detection/detect-active-doc.js +84 -0
  18. package/dist/detection/detection-client.js +62 -0
  19. package/dist/detection/face-contour-geometry.js +137 -0
  20. package/dist/detection/face-detector.js +34 -0
  21. package/dist/detection/face-feature-geometry.js +180 -0
  22. package/dist/detection/geometry.js +35 -0
  23. package/dist/detection/landmark-detection-client.js +38 -0
  24. package/dist/detection/landmark-detector.js +63 -0
  25. package/dist/detection/landmark-spec.js +29 -0
  26. package/dist/detection/mesh-face.js +36 -0
  27. package/dist/detection/object-detector.js +135 -0
  28. package/dist/detection/runtime.js +46 -0
  29. package/dist/install-channel.js +8 -0
  30. package/dist/models/dfine-s-coco-quant.onnx +0 -0
  31. package/dist/models/ultraface-rfb-320.onnx +0 -0
  32. package/dist/modules/ce/index.js +23 -5
  33. package/dist/perception/facets.js +148 -0
  34. package/dist/perception/region-precompute.js +122 -0
  35. package/dist/perception/region-scorer.js +151 -0
  36. package/dist/perception/scene-model.js +156 -0
  37. package/dist/perception/select-recipes.js +493 -0
  38. package/dist/perception/sky-ground-flood.js +234 -0
  39. package/dist/perception/sky-mask-transfer.js +77 -0
  40. package/dist/platform/macos-executor.js +0 -1
  41. package/dist/platform/windows-executor.js +6 -1
  42. package/dist/preview/coordinate-grid.js +250 -0
  43. package/dist/skills/editmamei-skill.zip +0 -0
  44. package/dist/telemetry/events.js +2 -1
  45. package/dist/telemetry/sanitize.js +15 -1
  46. package/dist/tools/adjustment-tools.js +46 -51
  47. package/dist/tools/detection-tools.js +209 -0
  48. package/dist/tools/document-tools.js +41 -30
  49. package/dist/tools/filter-tools.js +616 -101
  50. package/dist/tools/guide-tools.js +186 -0
  51. package/dist/tools/history-tools.js +1 -39
  52. package/dist/tools/image-tools.js +88 -0
  53. package/dist/tools/inspect-tools.js +86 -0
  54. package/dist/tools/layer-properties-tools.js +195 -106
  55. package/dist/tools/layer-tools.js +55 -14
  56. package/dist/tools/layer-transform-tools.js +236 -73
  57. package/dist/tools/metadata-tools.js +2 -32
  58. package/dist/tools/object-tools.js +193 -0
  59. package/dist/tools/overview-tools.js +69 -43
  60. package/dist/tools/path-tools.js +274 -0
  61. package/dist/tools/portrait-tools.js +146 -0
  62. package/dist/tools/preview-tools.js +78 -3
  63. package/dist/tools/retouch-tools.js +45 -33
  64. package/dist/tools/scene-tools.js +401 -0
  65. package/dist/tools/selection-tools.js +766 -228
  66. package/dist/tools/text-on-object-tools.js +225 -0
  67. package/dist/tools/text-tools.js +47 -56
  68. package/dist/tools/transform-canvas-tools.js +138 -0
  69. package/dist/tools/vector-mask-tools.js +116 -0
  70. package/dist/update/check.js +77 -0
  71. package/dist/version.js +1 -1
  72. package/package.json +4 -2
@@ -0,0 +1,178 @@
1
+ export const GROUPS = {
2
+ core: {
3
+ id: 'core',
4
+ label: 'Core',
5
+ purpose: 'Session liveness, orientation, and undo/redo — always available.',
6
+ },
7
+ inspect: {
8
+ id: 'inspect',
9
+ label: 'Inspect',
10
+ purpose: 'Read document/layer/selection/history state and render previews.',
11
+ },
12
+ verify: {
13
+ id: 'verify',
14
+ label: 'Verify',
15
+ purpose: 'Measurement primitives — histogram, region compare, bounds diff — to check work instead of eyeballing.',
16
+ },
17
+ document: {
18
+ id: 'document',
19
+ label: 'Document & canvas',
20
+ purpose: 'Open/create/save/export documents; resize/crop/convert/rotate the canvas; place images; guides.',
21
+ },
22
+ select: {
23
+ id: 'select',
24
+ label: 'Selection',
25
+ purpose: 'Manual selections, selection edits, and alpha-channel save/load.',
26
+ },
27
+ select_ai: {
28
+ id: 'select_ai',
29
+ label: 'AI selection',
30
+ purpose: 'Sensei/CV-backed subject & sky selection (Pro).',
31
+ },
32
+ adjust: {
33
+ id: 'adjust',
34
+ label: 'Adjustments',
35
+ purpose: 'Non-destructive adjustment layers + the destructive tonal bakes with no layer equivalent.',
36
+ },
37
+ filter: {
38
+ id: 'filter',
39
+ label: 'Filters',
40
+ purpose: 'Blur/sharpen/noise/stylize filters (auto-duplicate-first by default).',
41
+ },
42
+ retouch: {
43
+ id: 'retouch',
44
+ label: 'Retouch',
45
+ purpose: 'Content-aware fill/patch/move and brush strokes.',
46
+ },
47
+ layers: {
48
+ id: 'layers',
49
+ label: 'Layers',
50
+ purpose: 'Layer lifecycle, properties, transforms, ordering, groups, and styles.',
51
+ },
52
+ masks: {
53
+ id: 'masks',
54
+ label: 'Masks & paths',
55
+ purpose: 'Layer masks, clipping masks, vector masks, and work paths.',
56
+ },
57
+ type: {
58
+ id: 'type',
59
+ label: 'Type',
60
+ purpose: 'Create and style text layers.',
61
+ },
62
+ perception: {
63
+ id: 'perception',
64
+ label: 'Perception',
65
+ purpose: 'On-device CV that gives the model real coordinates/selections — detection, scene read, and detection-driven edits.',
66
+ },
67
+ face: {
68
+ id: 'face',
69
+ label: 'Face mesh (Pro)',
70
+ purpose: 'Face-landmark perception and mesh-aimed feature selection / contour strokes (Pro).',
71
+ },
72
+ templates: {
73
+ id: 'templates',
74
+ label: 'Templates (Pro)',
75
+ purpose: 'Save, recall, apply, and verify reproducible aesthetic recipes (Pro).',
76
+ },
77
+ automation: {
78
+ id: 'automation',
79
+ label: 'Automation (Pro)',
80
+ purpose: 'Play recorded Actions and the ExtendScript escape hatch (Pro).',
81
+ },
82
+ };
83
+ export const TOOL_GROUPS = {
84
+ photoshop_ping: 'core',
85
+ photoshop_overview: 'core',
86
+ photoshop_undo: 'core',
87
+ photoshop_redo: 'core',
88
+ photoshop_list_capabilities: 'core',
89
+ photoshop_inspect: 'inspect',
90
+ photoshop_get_preview: 'inspect',
91
+ photoshop_get_histogram: 'verify',
92
+ photoshop_compare_regions: 'verify',
93
+ photoshop_get_layer_bounds_diff: 'verify',
94
+ photoshop_get_selection_preview: 'verify',
95
+ photoshop_create_document: 'document',
96
+ photoshop_open_document: 'document',
97
+ photoshop_close_document: 'document',
98
+ photoshop_save_psd: 'document',
99
+ photoshop_export: 'document',
100
+ photoshop_place_image: 'document',
101
+ photoshop_resize_image: 'document',
102
+ photoshop_crop_document: 'document',
103
+ photoshop_convert_image_mode: 'document',
104
+ photoshop_transform_canvas: 'document',
105
+ photoshop_guides: 'document',
106
+ photoshop_select: 'select',
107
+ photoshop_modify_selection: 'select',
108
+ photoshop_selection_channel: 'select',
109
+ photoshop_select_subject: 'select_ai',
110
+ photoshop_select_sky: 'select_ai',
111
+ photoshop_select_subject_instance: 'select_ai',
112
+ photoshop_add_adjustment_layer: 'adjust',
113
+ photoshop_apply_adjustment: 'adjust',
114
+ photoshop_apply_filter: 'filter',
115
+ photoshop_retouch: 'retouch',
116
+ photoshop_apply_brush_stroke: 'retouch',
117
+ photoshop_create_layer: 'layers',
118
+ photoshop_delete_layer: 'layers',
119
+ photoshop_fill_layer: 'layers',
120
+ photoshop_add_fill_layer: 'layers',
121
+ photoshop_select_layer: 'layers',
122
+ photoshop_move_layer_to_position: 'layers',
123
+ photoshop_duplicate_layer: 'layers',
124
+ photoshop_layer_via_copy: 'layers',
125
+ photoshop_convert_to_smart_object: 'layers',
126
+ photoshop_rasterize_layer: 'layers',
127
+ photoshop_set_layer: 'layers',
128
+ photoshop_merge: 'layers',
129
+ photoshop_bake_layer: 'layers',
130
+ photoshop_add_layer_style: 'layers',
131
+ photoshop_transform_layer: 'layers',
132
+ photoshop_warp_layer: 'layers',
133
+ photoshop_warp_layer_mesh: 'layers',
134
+ photoshop_create_group: 'layers',
135
+ photoshop_move_layer_to_group: 'layers',
136
+ photoshop_set_group_blend_mode: 'layers',
137
+ photoshop_ungroup: 'layers',
138
+ photoshop_delete_group: 'layers',
139
+ photoshop_layer_mask: 'masks',
140
+ photoshop_create_clipping_mask: 'masks',
141
+ photoshop_release_clipping_mask: 'masks',
142
+ photoshop_path: 'masks',
143
+ photoshop_vector_mask: 'masks',
144
+ photoshop_create_text_layer: 'type',
145
+ photoshop_set_text: 'type',
146
+ photoshop_detect: 'perception',
147
+ photoshop_scene: 'perception',
148
+ photoshop_select_by_reference: 'perception',
149
+ photoshop_object: 'perception',
150
+ photoshop_portrait_touchup: 'perception',
151
+ photoshop_text_on_object: 'perception',
152
+ photoshop_detect_landmarks: 'face',
153
+ photoshop_select_face_feature: 'face',
154
+ photoshop_stroke_face_contour: 'face',
155
+ photoshop_template_create_evidence: 'templates',
156
+ photoshop_template_save: 'templates',
157
+ photoshop_template_list: 'templates',
158
+ photoshop_template_apply: 'templates',
159
+ photoshop_template_verify: 'templates',
160
+ photoshop_template_recall: 'templates',
161
+ photoshop_template_delete: 'templates',
162
+ photoshop_list_actions: 'automation',
163
+ photoshop_play_action: 'automation',
164
+ photoshop_execute_script: 'automation',
165
+ };
166
+ export function groupOf(toolName) {
167
+ const group = TOOL_GROUPS[toolName];
168
+ if (!group) {
169
+ throw new Error(`Tool '${toolName}' has no entry in src/core/tool-groups.ts. ` +
170
+ `Every registered tool must be assigned a capability group.`);
171
+ }
172
+ return group;
173
+ }
174
+ export function toolsInGroup(group) {
175
+ return Object.entries(TOOL_GROUPS)
176
+ .filter(([, g]) => g === group)
177
+ .map(([name]) => name);
178
+ }
@@ -1,26 +1,17 @@
1
1
  export const TOOL_TIERS = {
2
2
  photoshop_ping: 'community',
3
+ photoshop_list_capabilities: 'community',
3
4
  photoshop_list_actions: 'pro',
4
5
  photoshop_play_action: 'pro',
5
6
  photoshop_execute_script: 'pro',
6
7
  photoshop_add_adjustment_layer: 'community',
7
- photoshop_apply_shadows_highlights: 'community',
8
- photoshop_apply_equalize: 'community',
9
- photoshop_apply_color_lookup: 'community',
8
+ photoshop_apply_adjustment: 'community',
10
9
  photoshop_create_document: 'community',
11
10
  photoshop_close_document: 'community',
12
11
  photoshop_open_document: 'community',
13
12
  photoshop_save_psd: 'community',
14
- photoshop_export_jpeg: 'community',
15
- photoshop_export_png: 'community',
16
- photoshop_apply_gaussian_blur: 'community',
17
- photoshop_apply_sharpen: 'community',
18
- photoshop_apply_noise: 'community',
19
- photoshop_apply_motion_blur: 'community',
20
- photoshop_apply_lens_blur: 'community',
21
- photoshop_apply_smart_sharpen: 'community',
22
- photoshop_apply_reduce_noise: 'community',
23
- photoshop_apply_high_pass: 'community',
13
+ photoshop_export: 'community',
14
+ photoshop_apply_filter: 'community',
24
15
  photoshop_create_group: 'community',
25
16
  photoshop_move_layer_to_group: 'community',
26
17
  photoshop_set_group_blend_mode: 'community',
@@ -30,59 +21,57 @@ export const TOOL_TIERS = {
30
21
  photoshop_release_clipping_mask: 'dev',
31
22
  photoshop_undo: 'community',
32
23
  photoshop_redo: 'community',
33
- photoshop_get_history: 'community',
34
24
  photoshop_place_image: 'community',
35
25
  photoshop_resize_image: 'community',
36
26
  photoshop_crop_document: 'community',
27
+ photoshop_convert_image_mode: 'community',
37
28
  photoshop_move_layer_to_position: 'community',
38
29
  photoshop_convert_to_smart_object: 'community',
39
30
  photoshop_rasterize_layer: 'community',
40
- photoshop_set_layer_opacity: 'community',
41
- photoshop_set_layer_blend_mode: 'community',
42
- photoshop_set_layer_visibility: 'community',
43
- photoshop_set_layer_locked: 'community',
44
- photoshop_rename_layer: 'community',
31
+ photoshop_set_layer: 'community',
45
32
  photoshop_duplicate_layer: 'community',
46
- photoshop_merge_visible_layers: 'community',
47
- photoshop_stamp_visible: 'community',
48
- photoshop_flatten_image: 'community',
33
+ photoshop_layer_via_copy: 'community',
34
+ photoshop_merge: 'community',
35
+ photoshop_bake_layer: 'community',
49
36
  photoshop_add_layer_style: 'community',
50
37
  photoshop_create_layer: 'community',
51
38
  photoshop_delete_layer: 'community',
52
39
  photoshop_create_text_layer: 'community',
53
40
  photoshop_fill_layer: 'community',
54
- photoshop_get_layer_tree: 'community',
41
+ photoshop_add_fill_layer: 'community',
55
42
  photoshop_select_layer: 'community',
56
- photoshop_fit_layer_to_document: 'community',
57
- photoshop_scale_layer: 'community',
58
- photoshop_move_layer: 'community',
59
- photoshop_rotate_layer: 'community',
60
- photoshop_get_metadata: 'community',
43
+ photoshop_transform_layer: 'community',
44
+ photoshop_warp_layer: 'pro',
45
+ photoshop_warp_layer_mesh: 'pro',
46
+ photoshop_transform_canvas: 'community',
47
+ photoshop_guides: 'community',
48
+ photoshop_inspect: 'community',
61
49
  photoshop_overview: 'community',
62
50
  photoshop_get_preview: 'community',
63
51
  photoshop_get_layer_bounds_diff: 'community',
64
52
  photoshop_compare_regions: 'community',
65
53
  photoshop_get_histogram: 'community',
66
- photoshop_apply_content_aware_fill: 'community',
67
- photoshop_apply_patch: 'community',
68
- photoshop_apply_content_aware_move: 'community',
54
+ photoshop_retouch: 'community',
69
55
  photoshop_apply_brush_stroke: 'dev',
70
- photoshop_select_rectangle: 'community',
56
+ photoshop_detect: 'community',
57
+ photoshop_object: 'dev',
58
+ photoshop_portrait_touchup: 'dev',
59
+ photoshop_text_on_object: 'dev',
60
+ photoshop_detect_landmarks: 'pro',
61
+ photoshop_select_face_feature: 'pro',
62
+ photoshop_stroke_face_contour: 'dev',
63
+ photoshop_scene: 'community',
64
+ photoshop_select_by_reference: 'community',
65
+ photoshop_select: 'community',
71
66
  photoshop_select_subject: 'pro',
72
67
  photoshop_select_sky: 'pro',
73
- photoshop_select_color_range: 'community',
74
- photoshop_magic_wand: 'community',
75
- photoshop_get_selection_info: 'community',
68
+ photoshop_select_subject_instance: 'pro',
69
+ photoshop_modify_selection: 'community',
76
70
  photoshop_get_selection_preview: 'community',
77
- photoshop_feather_selection: 'community',
78
- photoshop_select_all: 'community',
79
- photoshop_deselect: 'community',
80
- photoshop_invert_selection: 'community',
81
- photoshop_save_selection_to_channel: 'community',
82
- photoshop_load_selection_from_channel: 'community',
83
- photoshop_create_layer_mask: 'community',
84
- photoshop_delete_layer_mask: 'community',
85
- photoshop_apply_layer_mask: 'community',
71
+ photoshop_selection_channel: 'community',
72
+ photoshop_layer_mask: 'community',
73
+ photoshop_path: 'community',
74
+ photoshop_vector_mask: 'community',
86
75
  photoshop_template_create_evidence: 'pro',
87
76
  photoshop_template_save: 'pro',
88
77
  photoshop_template_list: 'pro',
@@ -90,10 +79,7 @@ export const TOOL_TIERS = {
90
79
  photoshop_template_verify: 'pro',
91
80
  photoshop_template_recall: 'pro',
92
81
  photoshop_template_delete: 'pro',
93
- photoshop_set_text_font: 'community',
94
- photoshop_set_text_color: 'community',
95
- photoshop_set_text_alignment: 'community',
96
- photoshop_update_text_content: 'community',
82
+ photoshop_set_text: 'community',
97
83
  };
98
84
  export function tierOf(toolName) {
99
85
  const tier = TOOL_TIERS[toolName];
@@ -27,6 +27,9 @@ export function unzipBundle(container) {
27
27
  const out = [];
28
28
  let total = 0;
29
29
  for (const e of entries) {
30
+ if (isUnsafeEntryName(e.entryName)) {
31
+ throw new BundleError(`bundle entry '${e.entryName}' escapes the install directory`);
32
+ }
30
33
  const declared = e.header?.size ?? 0;
31
34
  if (declared > MAX_ENTRY_BYTES) {
32
35
  throw new BundleError(`bundle entry '${e.entryName}' is too large (${declared} bytes)`);
@@ -68,6 +71,11 @@ export function safeResolve(root, name) {
68
71
  }
69
72
  return out;
70
73
  }
74
+ export function isUnsafeEntryName(name) {
75
+ if (isAbsolute(name) || /^[A-Za-z]:[\\/]/.test(name) || /^[\\/]{2}/.test(name))
76
+ return true;
77
+ return name.split(/[\\/]/).some((seg) => seg === '..');
78
+ }
71
79
  function isCoreBinary(name) {
72
80
  const base = name.split('/').pop() ?? name;
73
81
  return base.startsWith('editmamei-core-');
@@ -1,5 +1,28 @@
1
1
  const DEFAULT_BASE_URL = 'https://editmamei-delivery.editmamei.workers.dev';
2
2
  export function resolveDeliveryConfig(url = process.env.EDITMAMEI_DELIVERY_URL) {
3
3
  const baseUrl = (url ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
4
+ assertSecureEndpoint(baseUrl);
4
5
  return { baseUrl };
5
6
  }
7
+ function assertSecureEndpoint(baseUrl) {
8
+ if (!baseUrl)
9
+ return;
10
+ let u;
11
+ try {
12
+ u = new URL(baseUrl);
13
+ }
14
+ catch {
15
+ throw new Error(`EDITMAMEI_DELIVERY_URL is not a valid URL: ${baseUrl}`);
16
+ }
17
+ if (u.protocol === 'https:')
18
+ return;
19
+ const host = u.hostname;
20
+ const isLoopback = host === 'localhost' ||
21
+ host === '::1' ||
22
+ host === '[::1]' ||
23
+ /^127(?:\.\d{1,3}){3}$/.test(host);
24
+ if (u.protocol === 'http:' && isLoopback)
25
+ return;
26
+ throw new Error(`EDITMAMEI_DELIVERY_URL must use https (got ${u.protocol}//${host}); ` +
27
+ 'plaintext http is allowed only for localhost during local development.');
28
+ }
@@ -25,7 +25,8 @@ export async function provisionModules(key, opts = {}) {
25
25
  result.notConfigured = true;
26
26
  return result;
27
27
  }
28
- throw e;
28
+ result.errors.push({ sku: '*', message: errMsg(e) });
29
+ return result;
29
30
  }
30
31
  let manifest;
31
32
  try {
@@ -54,6 +55,14 @@ export async function provisionModules(key, opts = {}) {
54
55
  result.skipped.push({ sku, version: latest, reason: 'up-to-date' });
55
56
  continue;
56
57
  }
58
+ if (installed && compareVersions(latest, installed.version) < 0) {
59
+ result.skipped.push({
60
+ sku,
61
+ version: latest,
62
+ reason: `rollback-blocked (installed v${installed.version} is newer than manifest latest v${latest})`,
63
+ });
64
+ continue;
65
+ }
57
66
  const vEntry = entry.versions[latest];
58
67
  if (!vEntry) {
59
68
  result.errors.push({ sku, message: `manifest has no descriptor for ${sku} v${latest}` });
@@ -82,7 +91,8 @@ export async function provisionModules(key, opts = {}) {
82
91
  });
83
92
  continue;
84
93
  }
85
- if (!verifyModuleSignature(sku, latest, digest, vEntry.sig, opts.signingKeys)) {
94
+ const sig = vEntry.sig;
95
+ if (!verifyModuleSignature(sku, latest, digest, sig, opts.signingKeys)) {
86
96
  result.errors.push({
87
97
  sku,
88
98
  message: `signature verification failed for ${sku} v${latest} — refusing to install unverified module code`,
@@ -97,6 +107,7 @@ export async function provisionModules(key, opts = {}) {
97
107
  sha256: digest,
98
108
  alg: contentKey.alg,
99
109
  content_key: contentKey.key,
110
+ sig: sig,
100
111
  }, blob, opts);
101
112
  result.installed.push({ sku, version: latest });
102
113
  }
@@ -109,3 +120,20 @@ export async function provisionModules(key, opts = {}) {
109
120
  function errMsg(e) {
110
121
  return e instanceof Error ? e.message : String(e);
111
122
  }
123
+ function compareVersions(a, b) {
124
+ const [aCore, aPre] = a.split('-', 2);
125
+ const [bCore, bPre] = b.split('-', 2);
126
+ const ap = aCore.split('.').map(Number);
127
+ const bp = bCore.split('.').map(Number);
128
+ for (let i = 0; i < 3; i++) {
129
+ if (ap[i] !== bp[i])
130
+ return ap[i] - bp[i];
131
+ }
132
+ if (aPre === bPre)
133
+ return 0;
134
+ if (aPre === undefined)
135
+ return 1;
136
+ if (bPre === undefined)
137
+ return -1;
138
+ return aPre < bPre ? -1 : 1;
139
+ }
@@ -2,12 +2,15 @@ import { join, dirname } from 'node:path';
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
3
3
  import { settingsDir } from '../core/settings.js';
4
4
  import { installBundle } from './bundle.js';
5
+ import { sha256Hex } from './crypto.js';
6
+ import { verifyModuleSignature, MODULE_SIGNING_PUBLIC_KEYS } from './signing.js';
5
7
  import { Logger } from '../utils/logger.js';
6
8
  const logger = new Logger('Modules');
7
9
  const INSTALLED_FILENAME = 'installed.json';
8
10
  export const PRO_SKU = 'pro';
9
11
  export const PRO_HANDLERS_ENTRY = 'pro-handlers.mjs';
10
12
  const MODULE_MANIFEST = 'manifest.json';
13
+ const MODULE_ARTIFACT = 'artifact.enc';
11
14
  export function modulesRoot(opts = {}) {
12
15
  return join(settingsDir(opts), 'modules');
13
16
  }
@@ -23,6 +26,9 @@ export function moduleHandlersPath(sku, version, opts = {}) {
23
26
  export function moduleBinDir(sku, version, opts = {}) {
24
27
  return join(installedModuleDir(sku, version, opts), 'bin');
25
28
  }
29
+ export function moduleArtifactPath(sku, version, opts = {}) {
30
+ return join(installedModuleDir(sku, version, opts), MODULE_ARTIFACT);
31
+ }
26
32
  function isInstalledModule(v) {
27
33
  if (typeof v !== 'object' || v === null)
28
34
  return false;
@@ -33,6 +39,7 @@ function isInstalledModule(v) {
33
39
  typeof r.sha256 === 'string' &&
34
40
  typeof r.alg === 'string' &&
35
41
  typeof r.content_key === 'string' &&
42
+ typeof r.sig === 'string' &&
36
43
  typeof r.installed_at === 'string');
37
44
  }
38
45
  export function readInstalledModule(sku, opts = {}) {
@@ -65,9 +72,41 @@ function writeFileAtomic(path, data) {
65
72
  renameSync(tmp, path);
66
73
  }
67
74
  export function installModule(rec, blob, opts = {}) {
68
- installBundle(blob, rec.content_key, installedModuleDir(rec.sku, rec.version, opts));
75
+ const dir = installedModuleDir(rec.sku, rec.version, opts);
76
+ installBundle(blob, rec.content_key, dir);
77
+ writeFileSync(moduleArtifactPath(rec.sku, rec.version, opts), Buffer.from(blob), { mode: 0o600 });
69
78
  const now = opts.now ?? Date.now;
70
79
  const installed = { ...rec, installed_at: new Date(now()).toISOString() };
71
80
  writeFileAtomic(installedPath(rec.sku, opts), JSON.stringify(installed, null, 2) + '\n');
72
81
  return installed;
73
82
  }
83
+ export function loadVerifiedModule(sku, opts = {}, pubKeys = MODULE_SIGNING_PUBLIC_KEYS) {
84
+ const rec = readInstalledModule(sku, opts);
85
+ if (!rec)
86
+ return null;
87
+ const version = rec.version;
88
+ const dir = installedModuleDir(sku, version, opts);
89
+ try {
90
+ const blob = readFileSync(moduleArtifactPath(sku, version, opts));
91
+ const digest = sha256Hex(blob);
92
+ if (digest !== rec.sha256) {
93
+ logger.warn(`module '${sku}' v${version} artifact hash changed since install — refusing to load`);
94
+ return null;
95
+ }
96
+ if (!verifyModuleSignature(sku, version, digest, rec.sig, pubKeys)) {
97
+ logger.warn(`module '${sku}' v${version} signature did not verify at boot — refusing to load`);
98
+ return null;
99
+ }
100
+ installBundle(blob, rec.content_key, dir);
101
+ return {
102
+ version,
103
+ dir,
104
+ handlersPath: moduleHandlersPath(sku, version, opts),
105
+ binDir: moduleBinDir(sku, version, opts),
106
+ };
107
+ }
108
+ catch (err) {
109
+ logger.warn(`module '${sku}' v${version} failed boot verification: ${err instanceof Error ? err.message : String(err)}`);
110
+ return null;
111
+ }
112
+ }
@@ -0,0 +1,84 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { getContextInfo } from '../api/extendscript/_helpers.js';
3
+ import { jsLit, jsNum } from '../utils/jsx.js';
4
+ import { runScript } from '../utils/run-script.js';
5
+ import { TempDir, userOwnedTempRoot } from '../utils/temp.js';
6
+ import { mapDetectionToDoc, } from './detection-client.js';
7
+ export async function detectActiveDoc(connection, client, opts) {
8
+ const maxDimension = opts.maxDimension ?? 1024;
9
+ const dir = process.platform === 'darwin'
10
+ ? await TempDir.createWithRoot(userOwnedTempRoot(), 'editmamei-detect-')
11
+ : await TempDir.create('editmamei-detect-');
12
+ try {
13
+ const tempPath = dir.path('detect.jpg');
14
+ const script = `
15
+ ${getContextInfo}
16
+ if (app.documents.length === 0) { throw new Error('No active document'); }
17
+ for (var __pi = app.documents.length - 1; __pi >= 0; __pi--) {
18
+ try {
19
+ var __pd = app.documents[__pi];
20
+ if (String(__pd.name).indexOf('__mcp_detect__') !== -1) {
21
+ try { __pd.close(SaveOptions.DONOTSAVECHANGES); } catch (e0) {}
22
+ }
23
+ } catch (e1) {}
24
+ }
25
+ var orig = app.activeDocument;
26
+ var origW = orig.width.as('px');
27
+ var origH = orig.height.as('px');
28
+ var dup = orig.duplicate(orig.name + ' __mcp_detect__');
29
+ try {
30
+ var w = dup.width.as('px');
31
+ var h = dup.height.as('px');
32
+ var longEdge = (w > h) ? w : h;
33
+ if (longEdge > ${jsNum(maxDimension, 1024)}) {
34
+ var scale = ${jsNum(maxDimension, 1024)} / longEdge;
35
+ dup.resizeImage(
36
+ UnitValue(Math.round(w * scale), 'px'),
37
+ UnitValue(Math.round(h * scale), 'px'),
38
+ null,
39
+ ResampleMethod.BICUBIC
40
+ );
41
+ }
42
+ var outFile = new File(${jsLit(tempPath)});
43
+ var opts = new JPEGSaveOptions();
44
+ opts.quality = 8;
45
+ opts.embedColorProfile = true;
46
+ opts.formatOptions = FormatOptions.STANDARDBASELINE;
47
+ dup.saveAs(outFile, opts, true, Extension.LOWERCASE);
48
+ if (!outFile.exists) {
49
+ try { dup.close(SaveOptions.DONOTSAVECHANGES); } catch (eD) {}
50
+ try { app.activeDocument = orig; } catch (eA) {}
51
+ throw new Error('JPEG saveAs reported success but no file at ' + outFile.fsName);
52
+ }
53
+ dup.close(SaveOptions.DONOTSAVECHANGES);
54
+ app.activeDocument = orig;
55
+ return { ok: true, doc_width: origW, doc_height: origH, context: getContextInfo() };
56
+ } catch (e) {
57
+ try { dup.close(SaveOptions.DONOTSAVECHANGES); } catch (e2) {}
58
+ try { app.activeDocument = orig; } catch (e3) {}
59
+ throw e;
60
+ }
61
+ `;
62
+ const exp = (await runScript(connection, script));
63
+ const docWidth = exp.doc_width ?? 0;
64
+ const docHeight = exp.doc_height ?? 0;
65
+ const raw = await client.detect(tempPath, {
66
+ faces: opts.faces,
67
+ objects: opts.objects,
68
+ faceThreshold: opts.faceThreshold,
69
+ objectThreshold: opts.objectThreshold,
70
+ maxObjects: opts.maxObjects,
71
+ });
72
+ let exportBytes = Buffer.alloc(0);
73
+ try {
74
+ exportBytes = await readFile(tempPath);
75
+ }
76
+ catch {
77
+ }
78
+ const result = mapDetectionToDoc(raw, docWidth, docHeight);
79
+ return { docWidth, docHeight, result, raw, context: exp.context, exportBytes };
80
+ }
81
+ finally {
82
+ await dir.cleanup();
83
+ }
84
+ }
@@ -0,0 +1,62 @@
1
+ import { detectFaces } from './face-detector.js';
2
+ import { detectObjects } from './object-detector.js';
3
+ export class OnnxDetectionClient {
4
+ async detect(imagePath, opts) {
5
+ const wantFaces = opts.faces ?? false;
6
+ const wantObjects = opts.objects ?? false;
7
+ const [faceRes, objRes] = await Promise.all([
8
+ wantFaces ? detectFaces(imagePath, opts.faceThreshold) : Promise.resolve(null),
9
+ wantObjects
10
+ ? detectObjects(imagePath, { threshold: opts.objectThreshold, maxObjects: opts.maxObjects })
11
+ : Promise.resolve(null),
12
+ ]);
13
+ const dims = faceRes ?? objRes;
14
+ const result = {
15
+ image: { width: dims?.width ?? 0, height: dims?.height ?? 0 },
16
+ backends: {},
17
+ };
18
+ if (faceRes) {
19
+ result.faces = faceRes.faces.map((f) => ({ bbox: f.bbox, confidence: f.confidence }));
20
+ result.backends.faces = 'ultraface';
21
+ }
22
+ if (objRes) {
23
+ result.objects = objRes.objects.map((o) => ({
24
+ label: o.label,
25
+ class_id: o.class_id,
26
+ bbox: o.bbox,
27
+ confidence: o.confidence,
28
+ }));
29
+ result.backends.objects = 'dfine-s';
30
+ }
31
+ return result;
32
+ }
33
+ }
34
+ export function mapDetectionToDoc(result, docWidth, docHeight) {
35
+ const sx = result.image.width ? docWidth / result.image.width : 1;
36
+ const sy = result.image.height ? docHeight / result.image.height : 1;
37
+ const clamp = (v, max) => Math.min(Math.max(v, 0), max);
38
+ const rect = (r) => {
39
+ const x1 = clamp(Math.round(r[0] * sx), docWidth);
40
+ const y1 = clamp(Math.round(r[1] * sy), docHeight);
41
+ const x2 = clamp(Math.round(r[2] * sx), docWidth);
42
+ const y2 = clamp(Math.round(r[3] * sy), docHeight);
43
+ return [Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2)];
44
+ };
45
+ const scaleFeatures = (feat) => ({
46
+ ...feat,
47
+ points: feat.points.map((p) => ({ x: p.x * sx, y: p.y * sy, z: p.z })),
48
+ });
49
+ const mapFace = (f) => ({
50
+ ...f,
51
+ bbox: rect(f.bbox),
52
+ ...(f.features ? { features: scaleFeatures(f.features) } : {}),
53
+ });
54
+ return {
55
+ image: { width: docWidth, height: docHeight },
56
+ backends: result.backends,
57
+ ...(result.faces ? { faces: result.faces.map(mapFace) } : {}),
58
+ ...(result.objects
59
+ ? { objects: result.objects.map((o) => ({ ...o, bbox: rect(o.bbox) })) }
60
+ : {}),
61
+ };
62
+ }