@openvulcan/dsh-vulcancode 0.0.0-stage → 0.4.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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -2
  3. package/assets/openvulcan-icon.svg +33 -0
  4. package/cordis.patch.yml +17 -0
  5. package/node_modules/@openvulcan/dsh-decision-guard/LICENSE +21 -0
  6. package/node_modules/@openvulcan/dsh-decision-guard/README.md +14 -0
  7. package/node_modules/@openvulcan/dsh-decision-guard/assets/decision-guard-icon.png +0 -0
  8. package/node_modules/@openvulcan/dsh-decision-guard/lib/calls.d.ts +40 -0
  9. package/node_modules/@openvulcan/dsh-decision-guard/lib/calls.js +150 -0
  10. package/node_modules/@openvulcan/dsh-decision-guard/lib/client.js +2193 -0
  11. package/node_modules/@openvulcan/dsh-decision-guard/lib/decide.d.ts +90 -0
  12. package/node_modules/@openvulcan/dsh-decision-guard/lib/decide.js +108 -0
  13. package/node_modules/@openvulcan/dsh-decision-guard/lib/index.d.ts +129 -0
  14. package/node_modules/@openvulcan/dsh-decision-guard/lib/index.js +572 -0
  15. package/node_modules/@openvulcan/dsh-decision-guard/lib/intent.d.ts +22 -0
  16. package/node_modules/@openvulcan/dsh-decision-guard/lib/intent.js +77 -0
  17. package/node_modules/@openvulcan/dsh-decision-guard/lib/panel.d.ts +158 -0
  18. package/node_modules/@openvulcan/dsh-decision-guard/lib/panel.js +340 -0
  19. package/node_modules/@openvulcan/dsh-decision-guard/lib/records.d.ts +133 -0
  20. package/node_modules/@openvulcan/dsh-decision-guard/lib/records.js +186 -0
  21. package/node_modules/@openvulcan/dsh-decision-guard/lib/review.d.ts +55 -0
  22. package/node_modules/@openvulcan/dsh-decision-guard/lib/review.js +202 -0
  23. package/node_modules/@openvulcan/dsh-decision-guard/lib/session-state.d.ts +62 -0
  24. package/node_modules/@openvulcan/dsh-decision-guard/lib/session-state.js +155 -0
  25. package/node_modules/@openvulcan/dsh-decision-guard/lib/version.d.ts +2 -0
  26. package/node_modules/@openvulcan/dsh-decision-guard/lib/version.js +4 -0
  27. package/node_modules/@openvulcan/dsh-decision-guard/package.json +68 -0
  28. package/node_modules/@openvulcan/dsh-vulcan-account/LICENSE +21 -0
  29. package/node_modules/@openvulcan/dsh-vulcan-account/assets/account-icon.png +0 -0
  30. package/node_modules/@openvulcan/dsh-vulcan-account/lib/client.js +565 -0
  31. package/node_modules/@openvulcan/dsh-vulcan-account/lib/index.d.ts +92 -0
  32. package/node_modules/@openvulcan/dsh-vulcan-account/lib/index.js +194 -0
  33. package/node_modules/@openvulcan/dsh-vulcan-account/package.json +57 -0
  34. package/node_modules/@openvulcan/dsh-vulcan-answers/LICENSE +21 -0
  35. package/node_modules/@openvulcan/dsh-vulcan-answers/assets/answers-icon.png +0 -0
  36. package/node_modules/@openvulcan/dsh-vulcan-answers/assets/prompts/answer-mode-interactive.md +79 -0
  37. package/node_modules/@openvulcan/dsh-vulcan-answers/assets/prompts/answer-mode-standard.md +1 -0
  38. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/answer-model.d.ts +258 -0
  39. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/answer-model.js +698 -0
  40. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/client.js +671 -0
  41. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/fences.d.ts +29 -0
  42. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/fences.js +70 -0
  43. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/index.d.ts +19 -0
  44. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/index.js +52 -0
  45. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/limits.d.ts +9 -0
  46. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/limits.js +9 -0
  47. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/prompt.d.ts +22 -0
  48. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/prompt.js +39 -0
  49. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/reply-format.d.ts +20 -0
  50. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/reply-format.js +17 -0
  51. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/session-state.d.ts +66 -0
  52. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/session-state.js +118 -0
  53. package/node_modules/@openvulcan/dsh-vulcan-answers/package.json +59 -0
  54. package/node_modules/@openvulcan/dsh-vulcan-luaskills/LICENSE +21 -0
  55. package/node_modules/@openvulcan/dsh-vulcan-luaskills/assets/luaskills-icon.svg +1 -0
  56. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/client.js +940 -0
  57. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/constants.d.ts +46 -0
  58. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/constants.js +51 -0
  59. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/engine.d.ts +55 -0
  60. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/engine.js +149 -0
  61. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/hub.d.ts +49 -0
  62. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/hub.js +116 -0
  63. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/index.d.ts +26 -0
  64. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/index.js +276 -0
  65. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/protocol.d.ts +65 -0
  66. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/protocol.js +2 -0
  67. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/runtime.d.ts +111 -0
  68. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/runtime.js +281 -0
  69. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/sdk-install.d.ts +26 -0
  70. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/sdk-install.js +87 -0
  71. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/settings.d.ts +79 -0
  72. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/settings.js +165 -0
  73. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/skills.d.ts +34 -0
  74. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/skills.js +61 -0
  75. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/tools.d.ts +59 -0
  76. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/tools.js +107 -0
  77. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/worker.d.ts +10 -0
  78. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/worker.js +140 -0
  79. package/node_modules/@openvulcan/dsh-vulcan-luaskills/package.json +57 -0
  80. package/node_modules/@openvulcan/dsh-vulcan-memory/LICENSE +21 -0
  81. package/node_modules/@openvulcan/dsh-vulcan-memory/assets/markdown-memory-guide.md +21 -0
  82. package/node_modules/@openvulcan/dsh-vulcan-memory/assets/memory-curator/SKILL.md +31 -0
  83. package/node_modules/@openvulcan/dsh-vulcan-memory/assets/memory-icon.png +0 -0
  84. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/client.js +1090 -0
  85. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/index.d.ts +30 -0
  86. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/index.js +240 -0
  87. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/inject.d.ts +30 -0
  88. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/inject.js +46 -0
  89. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/constants.d.ts +23 -0
  90. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/constants.js +25 -0
  91. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/document.d.ts +108 -0
  92. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/document.js +373 -0
  93. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/errors.d.ts +14 -0
  94. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/errors.js +31 -0
  95. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/index.d.ts +196 -0
  96. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/index.js +204 -0
  97. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/operations.d.ts +92 -0
  98. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/operations.js +472 -0
  99. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/package.d.ts +126 -0
  100. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/package.js +553 -0
  101. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/registry.d.ts +56 -0
  102. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/registry.js +182 -0
  103. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/scope.d.ts +13 -0
  104. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/scope.js +24 -0
  105. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/search.d.ts +65 -0
  106. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/search.js +227 -0
  107. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/snapshot.d.ts +34 -0
  108. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/snapshot.js +171 -0
  109. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/store.d.ts +72 -0
  110. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/store.js +250 -0
  111. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/timestamp.d.ts +6 -0
  112. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/timestamp.js +12 -0
  113. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/tokens.d.ts +8 -0
  114. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/tokens.js +38 -0
  115. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/page.d.ts +132 -0
  116. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/page.js +235 -0
  117. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/project.d.ts +14 -0
  118. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/project.js +27 -0
  119. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/settings.d.ts +29 -0
  120. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/settings.js +62 -0
  121. package/node_modules/@openvulcan/dsh-vulcan-memory/package.json +63 -0
  122. package/node_modules/@openvulcan/dsh-vulcan-style/LICENSE +21 -0
  123. package/node_modules/@openvulcan/dsh-vulcan-style/assets/style-icon.png +0 -0
  124. package/node_modules/@openvulcan/dsh-vulcan-style/lib/client.js +1061 -0
  125. package/node_modules/@openvulcan/dsh-vulcan-style/lib/index.d.ts +34 -0
  126. package/node_modules/@openvulcan/dsh-vulcan-style/lib/index.js +42 -0
  127. package/node_modules/@openvulcan/dsh-vulcan-style/package.json +51 -0
  128. package/node_modules/@openvulcan/openvulcan-account/LICENSE +21 -0
  129. package/node_modules/@openvulcan/openvulcan-account/lib/account.d.ts +140 -0
  130. package/node_modules/@openvulcan/openvulcan-account/lib/account.js +347 -0
  131. package/node_modules/@openvulcan/openvulcan-account/lib/index.d.ts +1 -0
  132. package/node_modules/@openvulcan/openvulcan-account/lib/index.js +1 -0
  133. package/node_modules/@openvulcan/openvulcan-account/package.json +29 -0
  134. package/node_modules/@openvulcan/vulcan-decision/LICENSE +21 -0
  135. package/node_modules/@openvulcan/vulcan-decision/README.md +23 -0
  136. package/node_modules/@openvulcan/vulcan-decision/bin/vulcan-decision.js +4 -0
  137. package/node_modules/@openvulcan/vulcan-decision/lib/cli.d.ts +5 -0
  138. package/node_modules/@openvulcan/vulcan-decision/lib/cli.js +267 -0
  139. package/node_modules/@openvulcan/vulcan-decision/lib/cloud/edge.d.ts +211 -0
  140. package/node_modules/@openvulcan/vulcan-decision/lib/cloud/edge.js +594 -0
  141. package/node_modules/@openvulcan/vulcan-decision/lib/download.d.ts +36 -0
  142. package/node_modules/@openvulcan/vulcan-decision/lib/download.js +99 -0
  143. package/node_modules/@openvulcan/vulcan-decision/lib/engine.d.ts +37 -0
  144. package/node_modules/@openvulcan/vulcan-decision/lib/engine.js +39 -0
  145. package/node_modules/@openvulcan/vulcan-decision/lib/hardware.d.ts +105 -0
  146. package/node_modules/@openvulcan/vulcan-decision/lib/hardware.js +207 -0
  147. package/node_modules/@openvulcan/vulcan-decision/lib/home.d.ts +26 -0
  148. package/node_modules/@openvulcan/vulcan-decision/lib/home.js +54 -0
  149. package/node_modules/@openvulcan/vulcan-decision/lib/index.d.ts +17 -0
  150. package/node_modules/@openvulcan/vulcan-decision/lib/index.js +17 -0
  151. package/node_modules/@openvulcan/vulcan-decision/lib/local/engine.d.ts +54 -0
  152. package/node_modules/@openvulcan/vulcan-decision/lib/local/engine.js +157 -0
  153. package/node_modules/@openvulcan/vulcan-decision/lib/local/models.d.ts +126 -0
  154. package/node_modules/@openvulcan/vulcan-decision/lib/local/models.js +261 -0
  155. package/node_modules/@openvulcan/vulcan-decision/lib/local/packs.d.ts +102 -0
  156. package/node_modules/@openvulcan/vulcan-decision/lib/local/packs.js +195 -0
  157. package/node_modules/@openvulcan/vulcan-decision/lib/local/worker.d.ts +61 -0
  158. package/node_modules/@openvulcan/vulcan-decision/lib/local/worker.js +123 -0
  159. package/node_modules/@openvulcan/vulcan-decision/lib/protocol.d.ts +74 -0
  160. package/node_modules/@openvulcan/vulcan-decision/lib/protocol.js +31 -0
  161. package/node_modules/@openvulcan/vulcan-decision/lib/render.d.ts +15 -0
  162. package/node_modules/@openvulcan/vulcan-decision/lib/render.js +33 -0
  163. package/node_modules/@openvulcan/vulcan-decision/lib/version.d.ts +2 -0
  164. package/node_modules/@openvulcan/vulcan-decision/lib/version.js +4 -0
  165. package/node_modules/@openvulcan/vulcan-decision/package.json +40 -0
  166. package/node_modules/@openvulcan/vulcan-decision/python/vulcan_decision_worker.py +363 -0
  167. package/node_modules/@openvulcan/vulcan-decision/runtime-packs/onnx-cpu.json +627 -0
  168. package/node_modules/@openvulcan/vulcan-decision/runtime-packs/onnx-cuda.json +423 -0
  169. package/node_modules/@openvulcan/vulcan-decision/runtime-packs/torch-cpu.json +1047 -0
  170. package/node_modules/@openvulcan/vulcan-decision/runtime-packs/torch-cu128.json +647 -0
  171. package/package.json +53 -4
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Host half of the VulcanCode look for dsh. The look itself runs in the browser (`client/`); the
3
+ * Host only owns the settings, because dsh persists third-party theme choices nowhere else.
4
+ */
5
+ import type { Context, Volatile } from '@deepseek-ai/cordis';
6
+ import Schema from '@deepseek-ai/schemastery';
7
+ export declare const name = "vulcan-style";
8
+ /**
9
+ * What the user chose; mirrored by `THEME_CHOICES` in `client/shared.ts`.
10
+ *
11
+ * - `vulcan-auto`: Vulcan light or dark, following the system colour scheme.
12
+ * - `vulcan-light` / `vulcan-dark` / `vulcan-command`: that Vulcan theme.
13
+ * - `dsh`: leave the theme to dsh's own Appearance setting. Picking a dsh theme in dsh's Appearance
14
+ * row records this choice.
15
+ */
16
+ export declare const THEME_CHOICES: readonly ["vulcan-auto", "vulcan-light", "vulcan-dark", "vulcan-command", "dsh"];
17
+ export type ThemeChoice = typeof THEME_CHOICES[number];
18
+ /**
19
+ * Every field is volatile: dsh's settings forms expose only volatile fields, and the browser applies
20
+ * a change without restarting the plugin.
21
+ */
22
+ export interface Config {
23
+ theme: Volatile<ThemeChoice>;
24
+ squareCorners: Volatile<boolean>;
25
+ }
26
+ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
27
+ theme: Schema<"vulcan-auto" | "vulcan-light" | "vulcan-dark" | "vulcan-command" | "dsh", "vulcan-auto" | "vulcan-light" | "vulcan-dark" | "vulcan-command" | "dsh", "volatile-defined">;
28
+ squareCorners: Schema<boolean, boolean, "volatile-defined">;
29
+ }>>, Schemastery.ObjectT<NoInfer<{
30
+ theme: Schema<"vulcan-auto" | "vulcan-light" | "vulcan-dark" | "vulcan-command" | "dsh", "vulcan-auto" | "vulcan-light" | "vulcan-dark" | "vulcan-command" | "dsh", "volatile-defined">;
31
+ squareCorners: Schema<boolean, boolean, "volatile-defined">;
32
+ }>>, "plain">;
33
+ /** Nothing runs on the Host; the browser half reads and writes this plugin's settings. */
34
+ export declare function apply(_ctx: Context, _config: Config): void;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Host half of the VulcanCode look for dsh. The look itself runs in the browser (`client/`); the
3
+ * Host only owns the settings, because dsh persists third-party theme choices nowhere else.
4
+ */
5
+ import Schema from '@deepseek-ai/schemastery';
6
+ export const name = 'vulcan-style';
7
+ /**
8
+ * What the user chose; mirrored by `THEME_CHOICES` in `client/shared.ts`.
9
+ *
10
+ * - `vulcan-auto`: Vulcan light or dark, following the system colour scheme.
11
+ * - `vulcan-light` / `vulcan-dark` / `vulcan-command`: that Vulcan theme.
12
+ * - `dsh`: leave the theme to dsh's own Appearance setting. Picking a dsh theme in dsh's Appearance
13
+ * row records this choice.
14
+ */
15
+ export const THEME_CHOICES = ['vulcan-auto', 'vulcan-light', 'vulcan-dark', 'vulcan-command', 'dsh'];
16
+ export const Config = Schema.object({
17
+ theme: Schema.union(THEME_CHOICES)
18
+ .default('vulcan-auto')
19
+ .description('Interface theme.')
20
+ .volatile(),
21
+ squareCorners: Schema.boolean()
22
+ .default(true)
23
+ .description('Square corners.')
24
+ .volatile(),
25
+ }).i18n({
26
+ en: {
27
+ theme: {
28
+ $desc: 'Interface theme. vulcan-auto follows the system light or dark scheme; dsh leaves the theme to dsh\'s Appearance setting.',
29
+ $inner: ['Vulcan, following the system', 'Vulcan light', 'Vulcan dark', 'Vulcan command', 'dsh theme'],
30
+ },
31
+ squareCorners: 'Square corners; works with any theme, dsh\'s own included.',
32
+ },
33
+ zh: {
34
+ theme: {
35
+ $desc: '界面主题。vulcan-auto 跟随系统明暗;dsh 表示由 dsh 的「外观」设置决定。',
36
+ $inner: ['Vulcan,跟随系统', 'Vulcan 亮色', 'Vulcan 暗色', 'Vulcan 指挥舱', 'dsh 主题'],
37
+ },
38
+ squareCorners: '直角外观,可与任何主题搭配,包括 dsh 自带主题。',
39
+ },
40
+ });
41
+ /** Nothing runs on the Host; the browser half reads and writes this plugin's settings. */
42
+ export function apply(_ctx, _config) { }
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@openvulcan/dsh-vulcan-style",
3
+ "version": "0.4.0",
4
+ "description": "The VulcanCode look for DeepSeek Harness: Vulcan light, dark and command themes with square corners.",
5
+ "license": "MIT",
6
+ "icon": "./assets/style-icon.png",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/OpenVulcan/dsh-vulcancode.git",
10
+ "directory": "packages/dsh-vulcan-style"
11
+ },
12
+ "type": "module",
13
+ "main": "lib/index.js",
14
+ "types": "lib/index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./lib/index.d.ts",
18
+ "default": "./lib/index.js"
19
+ },
20
+ "./client": "./lib/client.js",
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "lib",
25
+ "!lib/**/*.test.*",
26
+ "assets"
27
+ ],
28
+ "engines": {
29
+ "dsh": ">=0.2.1-rc.2 <0.3.0"
30
+ },
31
+ "dsh": {
32
+ "client": {
33
+ "platform": "web",
34
+ "inject": [
35
+ "@deepseek-ai/dsh-api-remotes",
36
+ "@deepseek-ai/dsh-client-locale",
37
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
38
+ "@deepseek-ai/dsh-client-ui-settings",
39
+ "@deepseek-ai/dsh-client-ui-settings-general",
40
+ "@deepseek-ai/dsh-client-ui-theme"
41
+ ]
42
+ }
43
+ },
44
+ "peerDependencies": {
45
+ "@deepseek-ai/cordis": "^4.0.5",
46
+ "@deepseek-ai/schemastery": "^3.18.5"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenVulcan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The OpenVulcan account of this device.
3
+ *
4
+ * Signing in is a PKCE (S256) flow with a loopback callback: the host opens the approval page in a
5
+ * browser, the user approves, the browser is sent back to `http://127.0.0.1:<port>/vulcancode/auth/callback`
6
+ * and the code is exchanged once for a device credential. The credential is an opaque token sent as
7
+ * `Authorization: Auth <token>` to the site's own auth host only. It lasts 14 days and is rotated
8
+ * by a single-use refresh once less than a day remains; a rotation that may have been interrupted
9
+ * forces a fresh sign-in instead of guessing which token is current.
10
+ *
11
+ * Where the credential is kept is up to the host: dsh keeps it in its credential store, the command
12
+ * line in an owner-only file. This module never logs or returns the token itself.
13
+ */
14
+ /** The account cannot supply a usable credential: not signed in, expired, or the sign-in failed. */
15
+ export declare class AccountError extends Error {
16
+ readonly code = "not_signed_in";
17
+ constructor(message: string);
18
+ }
19
+ /** OpenVulcan deployments. The account's site is fixed when it signs in. */
20
+ export declare const SITES: {
21
+ readonly cn: {
22
+ readonly auth: "https://auth.openvulcan.cn";
23
+ readonly api: "https://api.openvulcan.cn";
24
+ };
25
+ readonly global: {
26
+ readonly auth: "https://auth.openvulcan.com";
27
+ readonly api: "https://api.openvulcan.com";
28
+ };
29
+ };
30
+ export type Site = keyof typeof SITES;
31
+ export declare function isSite(value: unknown): value is Site;
32
+ /** The stored device credential. */
33
+ export interface Credential {
34
+ readonly site: Site;
35
+ /** Origin of the auth host, kept so tests can sign in against a local authority. */
36
+ readonly auth_origin: string;
37
+ readonly access_token: string;
38
+ readonly authorization_id: string;
39
+ /** RFC 3339. */
40
+ readonly expires_at: string;
41
+ /** Set while a refresh is in flight; still set afterwards means the rotation was interrupted. */
42
+ readonly rotation_pending?: boolean;
43
+ }
44
+ /** Where a host keeps the credential. */
45
+ export interface CredentialStore {
46
+ read(): Promise<Credential | undefined>;
47
+ write(credential: Credential): Promise<void>;
48
+ delete(): Promise<void>;
49
+ }
50
+ /** An owner-only JSON file, for the command line. */
51
+ export declare class FileCredentialStore implements CredentialStore {
52
+ private readonly file;
53
+ constructor(file: string);
54
+ read(): Promise<Credential | undefined>;
55
+ write(credential: Credential): Promise<void>;
56
+ delete(): Promise<void>;
57
+ }
58
+ /** Accept a stored credential only when every field has its expected shape. */
59
+ export declare function parseCredential(value: unknown): Credential | undefined;
60
+ /** Refresh once less than this remains. */
61
+ export declare const REFRESH_AHEAD_MS: number;
62
+ /** What the user sees about the device on the approval page. */
63
+ export interface DeviceInfo {
64
+ readonly display_name: string;
65
+ readonly client_version: string;
66
+ }
67
+ /** The account as a host shows it; never contains the token. */
68
+ export interface AccountStatus {
69
+ readonly signedIn: boolean;
70
+ readonly site?: Site;
71
+ readonly authorizationId?: string;
72
+ readonly expiresAt?: string;
73
+ /** A refresh was interrupted; the user has to sign in again. */
74
+ readonly reauthRequired?: boolean;
75
+ }
76
+ /** A sign-in waiting for the user's approval. */
77
+ export interface PendingSignIn {
78
+ readonly approvalUrl: string;
79
+ readonly expiresAt: string;
80
+ /** Settles when the user approved (the credential is stored) or the sign-in failed. */
81
+ readonly done: Promise<AccountStatus>;
82
+ /** Stop waiting; a credential that already arrived is revoked. */
83
+ cancel(): void;
84
+ }
85
+ /** The signed-in user as OpenVulcan names them, and the plan in force. */
86
+ export interface AccountProfile {
87
+ /** Decimal public user id, kept as a string. */
88
+ readonly id: string;
89
+ readonly username: string;
90
+ readonly nickname: string;
91
+ /** The membership tier, or the free tier when not subscribed. */
92
+ readonly plan: string;
93
+ }
94
+ /** What a judge needs from an account: its state and a usable credential. */
95
+ export interface AccountSource {
96
+ status(): Promise<AccountStatus>;
97
+ /** @throws AccountError when there is no usable credential. */
98
+ credential(): Promise<Credential>;
99
+ }
100
+ export declare class OpenVulcanAccount implements AccountSource {
101
+ private readonly store;
102
+ private readonly device;
103
+ /** Overrides the auth origin of every site; tests point it at a local authority. */
104
+ private readonly authOrigin?;
105
+ private refreshing;
106
+ constructor(store: CredentialStore, device: DeviceInfo,
107
+ /** Overrides the auth origin of every site; tests point it at a local authority. */
108
+ authOrigin?: string | undefined);
109
+ status(): Promise<AccountStatus>;
110
+ /** Begin signing in to `site`. */
111
+ signIn(site: Site): Promise<PendingSignIn>;
112
+ /**
113
+ * Who is signed in, read from `GET /api/v1/vulcancode/account`; an answer for another device
114
+ * authorization is refused.
115
+ * @throws AccountError when nobody is signed in or the answer breaks the contract.
116
+ */
117
+ profile(): Promise<AccountProfile>;
118
+ /** Revoke the credential on the server and forget it here. */
119
+ signOut(): Promise<void>;
120
+ /**
121
+ * A usable credential, refreshed first when less than a day remains.
122
+ * @throws AccountError when there is none or it can no longer be trusted.
123
+ */
124
+ credential(): Promise<Credential>;
125
+ private refresh;
126
+ }
127
+ /** An HTTP answer outside 2xx. */
128
+ export declare class HttpError extends Error {
129
+ readonly status: number;
130
+ readonly code: string | undefined;
131
+ readonly retryAfterMs?: number | undefined;
132
+ constructor(status: number, code: string | undefined, message: string, retryAfterMs?: number | undefined);
133
+ }
134
+ /** POST JSON, with `Authorization: Auth <token>` when a token is given. */
135
+ export declare function postJson(url: string, token: string | undefined, body: unknown): Promise<unknown>;
136
+ export declare function requestJson(url: string, token: string | undefined, init: {
137
+ method: string;
138
+ body?: string;
139
+ }): Promise<unknown>;
140
+ export declare function retryAfter(header: string | null): number | undefined;
@@ -0,0 +1,347 @@
1
+ /**
2
+ * The OpenVulcan account of this device.
3
+ *
4
+ * Signing in is a PKCE (S256) flow with a loopback callback: the host opens the approval page in a
5
+ * browser, the user approves, the browser is sent back to `http://127.0.0.1:<port>/vulcancode/auth/callback`
6
+ * and the code is exchanged once for a device credential. The credential is an opaque token sent as
7
+ * `Authorization: Auth <token>` to the site's own auth host only. It lasts 14 days and is rotated
8
+ * by a single-use refresh once less than a day remains; a rotation that may have been interrupted
9
+ * forces a fresh sign-in instead of guessing which token is current.
10
+ *
11
+ * Where the credential is kept is up to the host: dsh keeps it in its credential store, the command
12
+ * line in an owner-only file. This module never logs or returns the token itself.
13
+ */
14
+ import { createHash, randomBytes } from 'node:crypto';
15
+ import fs from 'node:fs';
16
+ import http from 'node:http';
17
+ import os from 'node:os';
18
+ /** The account cannot supply a usable credential: not signed in, expired, or the sign-in failed. */
19
+ export class AccountError extends Error {
20
+ code = 'not_signed_in';
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = 'AccountError';
24
+ }
25
+ }
26
+ /** OpenVulcan deployments. The account's site is fixed when it signs in. */
27
+ export const SITES = {
28
+ cn: { auth: 'https://auth.openvulcan.cn', api: 'https://api.openvulcan.cn' },
29
+ global: { auth: 'https://auth.openvulcan.com', api: 'https://api.openvulcan.com' },
30
+ };
31
+ export function isSite(value) {
32
+ return value === 'cn' || value === 'global';
33
+ }
34
+ /** An owner-only JSON file, for the command line. */
35
+ export class FileCredentialStore {
36
+ file;
37
+ constructor(file) {
38
+ this.file = file;
39
+ }
40
+ async read() {
41
+ try {
42
+ return parseCredential(JSON.parse(fs.readFileSync(this.file, 'utf8')));
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ async write(credential) {
49
+ writeFileAtomic(this.file, `${JSON.stringify(credential, null, 2)}\n`);
50
+ }
51
+ async delete() {
52
+ fs.rmSync(this.file, { force: true });
53
+ }
54
+ }
55
+ function writeFileAtomic(file, text) {
56
+ const temporary = `${file}.${process.pid}.tmp`;
57
+ fs.writeFileSync(temporary, text, { mode: 0o600 });
58
+ fs.renameSync(temporary, file);
59
+ }
60
+ /** Accept a stored credential only when every field has its expected shape. */
61
+ export function parseCredential(value) {
62
+ const record = value;
63
+ if (typeof record !== 'object' || record === null)
64
+ return undefined;
65
+ if (!isSite(record.site) || typeof record.auth_origin !== 'string' || typeof record.access_token !== 'string'
66
+ || typeof record.authorization_id !== 'string' || typeof record.expires_at !== 'string' || Number.isNaN(Date.parse(record.expires_at))) {
67
+ return undefined;
68
+ }
69
+ return {
70
+ site: record.site, auth_origin: record.auth_origin, access_token: record.access_token,
71
+ authorization_id: record.authorization_id, expires_at: record.expires_at,
72
+ ...record.rotation_pending === true ? { rotation_pending: true } : {},
73
+ };
74
+ }
75
+ /** Refresh once less than this remains. */
76
+ export const REFRESH_AHEAD_MS = 24 * 60 * 60_000;
77
+ const CALLBACK_PATH = '/vulcancode/auth/callback';
78
+ const ACCESS_TOKEN = /^[A-Za-z0-9_-]{43}$/;
79
+ const REQUEST_TIMEOUT_MS = 15_000;
80
+ export class OpenVulcanAccount {
81
+ store;
82
+ device;
83
+ authOrigin;
84
+ refreshing;
85
+ constructor(store, device,
86
+ /** Overrides the auth origin of every site; tests point it at a local authority. */
87
+ authOrigin) {
88
+ this.store = store;
89
+ this.device = device;
90
+ this.authOrigin = authOrigin;
91
+ }
92
+ async status() {
93
+ const credential = await this.store.read();
94
+ if (credential === undefined)
95
+ return { signedIn: false };
96
+ return {
97
+ signedIn: credential.rotation_pending !== true && Date.parse(credential.expires_at) > Date.now(),
98
+ site: credential.site,
99
+ authorizationId: credential.authorization_id,
100
+ expiresAt: credential.expires_at,
101
+ ...credential.rotation_pending === true ? { reauthRequired: true } : {},
102
+ };
103
+ }
104
+ /** Begin signing in to `site`. */
105
+ async signIn(site) {
106
+ const origin = this.authOrigin ?? SITES[site].auth;
107
+ const state = base64url(randomBytes(32));
108
+ const verifier = base64url(randomBytes(32));
109
+ const challenge = base64url(createHash('sha256').update(verifier).digest());
110
+ let settle;
111
+ const callback = new Promise((resolve, reject) => {
112
+ settle = { resolve, reject };
113
+ });
114
+ const server = http.createServer((request, response) => {
115
+ const url = new URL(request.url ?? '/', 'http://127.0.0.1');
116
+ const keys = [...url.searchParams.keys()].sort();
117
+ if (request.method !== 'GET' || url.pathname !== CALLBACK_PATH || keys.join(',') !== 'code,state' || url.searchParams.get('state') !== state) {
118
+ response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' }).end('Invalid sign-in callback.');
119
+ return;
120
+ }
121
+ response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
122
+ .end('<!doctype html><meta charset="utf-8"><title>Vulcan Decision</title><p>Signed in. You can close this page.</p>');
123
+ settle.resolve(url.searchParams.get('code') ?? '');
124
+ });
125
+ await new Promise((resolve, reject) => {
126
+ server.once('error', reject);
127
+ server.listen(0, '127.0.0.1', () => resolve());
128
+ });
129
+ const port = server.address().port;
130
+ const close = () => server.close();
131
+ let begin;
132
+ try {
133
+ begin = await postJson(`${origin}/api/v1/vulcancode/authorize`, undefined, {
134
+ callback_uri: `http://127.0.0.1:${port}${CALLBACK_PATH}`,
135
+ state,
136
+ code_challenge: challenge,
137
+ code_challenge_method: 'S256',
138
+ device: {
139
+ display_name: this.device.display_name.slice(0, 64),
140
+ operating_system: operatingSystem(),
141
+ architecture: architecture(),
142
+ client_version: this.device.client_version.slice(0, 32),
143
+ },
144
+ });
145
+ checkApprovalUrl(begin.approval_url, origin, state);
146
+ }
147
+ catch (error) {
148
+ close();
149
+ throw error;
150
+ }
151
+ let cancelled = false;
152
+ const timeout = setTimeout(() => settle.reject(new AccountError('the sign-in request expired')), Math.max(0, Date.parse(begin.expires_at) - Date.now()) || 600_000);
153
+ const done = (async () => {
154
+ try {
155
+ const code = await callback;
156
+ const answer = await postJson(`${origin}/api/v1/vulcancode/token`, undefined, { code, code_verifier: verifier });
157
+ const credential = tokenAnswer(answer, site, origin);
158
+ if (cancelled) {
159
+ await revoke(credential);
160
+ throw new AccountError('the sign-in was cancelled');
161
+ }
162
+ await this.store.write(credential);
163
+ return await this.status();
164
+ }
165
+ finally {
166
+ clearTimeout(timeout);
167
+ close();
168
+ }
169
+ })();
170
+ done.catch(() => { });
171
+ return {
172
+ approvalUrl: begin.approval_url,
173
+ expiresAt: begin.expires_at,
174
+ done,
175
+ cancel: () => {
176
+ cancelled = true;
177
+ settle.reject(new AccountError('the sign-in was cancelled'));
178
+ },
179
+ };
180
+ }
181
+ /**
182
+ * Who is signed in, read from `GET /api/v1/vulcancode/account`; an answer for another device
183
+ * authorization is refused.
184
+ * @throws AccountError when nobody is signed in or the answer breaks the contract.
185
+ */
186
+ async profile() {
187
+ const credential = await this.credential();
188
+ const answer = await requestJson(`${credential.auth_origin}/api/v1/vulcancode/account`, credential.access_token, { method: 'GET' });
189
+ const user = answer?.user;
190
+ const tier = answer?.quota?.member?.tier ?? answer?.quota?.free?.tier;
191
+ if (typeof user?.id !== 'string' || typeof user.username !== 'string' || typeof user.nickname !== 'string' || typeof tier !== 'string') {
192
+ throw new AccountError('OpenVulcan answered a malformed account');
193
+ }
194
+ if (answer.authorization_id !== credential.authorization_id)
195
+ throw new AccountError('OpenVulcan answered the account of another device authorization');
196
+ return { id: user.id, username: user.username, nickname: user.nickname, plan: tier };
197
+ }
198
+ /** Revoke the credential on the server and forget it here. */
199
+ async signOut() {
200
+ const credential = await this.store.read();
201
+ await this.store.delete();
202
+ if (credential !== undefined)
203
+ await revoke(credential);
204
+ }
205
+ /**
206
+ * A usable credential, refreshed first when less than a day remains.
207
+ * @throws AccountError when there is none or it can no longer be trusted.
208
+ */
209
+ async credential() {
210
+ const credential = await this.store.read();
211
+ if (credential === undefined)
212
+ throw new AccountError('not signed in to OpenVulcan');
213
+ if (credential.rotation_pending === true)
214
+ throw new AccountError('a credential rotation was interrupted; sign in to OpenVulcan again');
215
+ const remaining = Date.parse(credential.expires_at) - Date.now();
216
+ if (remaining <= 0)
217
+ throw new AccountError('the OpenVulcan sign-in expired; sign in again');
218
+ if (remaining > REFRESH_AHEAD_MS)
219
+ return credential;
220
+ this.refreshing ??= this.refresh(credential).finally(() => {
221
+ this.refreshing = undefined;
222
+ });
223
+ return await this.refreshing;
224
+ }
225
+ async refresh(credential) {
226
+ await this.store.write({ ...credential, rotation_pending: true });
227
+ let answer;
228
+ try {
229
+ answer = await postJson(`${credential.auth_origin}/api/v1/vulcancode/token/refresh`, credential.access_token, {});
230
+ }
231
+ catch (error) {
232
+ // A refusal means the credential is gone; anything else leaves the rotation pending.
233
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403))
234
+ await this.store.delete();
235
+ throw new AccountError(`refreshing the OpenVulcan sign-in failed: ${error.message}`);
236
+ }
237
+ const next = tokenAnswer(answer, credential.site, credential.auth_origin);
238
+ if (next.authorization_id !== credential.authorization_id) {
239
+ await this.store.delete();
240
+ throw new AccountError('the refreshed credential belongs to another device authorization');
241
+ }
242
+ await this.store.write(next);
243
+ return next;
244
+ }
245
+ }
246
+ async function revoke(credential) {
247
+ try {
248
+ await postJson(`${credential.auth_origin}/api/v1/vulcancode/logout`, credential.access_token, {});
249
+ }
250
+ catch (error) {
251
+ // An already invalid credential is as good as revoked.
252
+ if (!(error instanceof HttpError && error.status === 401))
253
+ throw error;
254
+ }
255
+ }
256
+ function tokenAnswer(value, site, origin) {
257
+ const answer = value;
258
+ if (typeof answer?.access_token !== 'string' || !ACCESS_TOKEN.test(answer.access_token) || answer.token_type !== 'Auth'
259
+ || typeof answer.expires_at !== 'string' || Number.isNaN(Date.parse(answer.expires_at))
260
+ || typeof answer.authorization_id !== 'string' || answer.authorization_id === '') {
261
+ throw new AccountError('OpenVulcan answered a malformed credential');
262
+ }
263
+ if (answer.site !== undefined && answer.site !== site)
264
+ throw new AccountError('OpenVulcan answered for another site');
265
+ return { site, auth_origin: origin, access_token: answer.access_token, authorization_id: answer.authorization_id, expires_at: answer.expires_at };
266
+ }
267
+ function checkApprovalUrl(value, origin, state) {
268
+ if (typeof value !== 'string')
269
+ throw new AccountError('OpenVulcan answered no approval page');
270
+ const url = new URL(value);
271
+ const keys = [...url.searchParams.keys()].sort().join(',');
272
+ if (url.origin !== origin || url.pathname !== '/authorize' || keys !== 'request_id,state' || url.searchParams.get('state') !== state) {
273
+ throw new AccountError('OpenVulcan answered an unexpected approval page');
274
+ }
275
+ }
276
+ /** An HTTP answer outside 2xx. */
277
+ export class HttpError extends Error {
278
+ status;
279
+ code;
280
+ retryAfterMs;
281
+ constructor(status, code, message, retryAfterMs) {
282
+ super(message);
283
+ this.status = status;
284
+ this.code = code;
285
+ this.retryAfterMs = retryAfterMs;
286
+ this.name = 'HttpError';
287
+ }
288
+ }
289
+ /** POST JSON, with `Authorization: Auth <token>` when a token is given. */
290
+ export async function postJson(url, token, body) {
291
+ return await requestJson(url, token, { method: 'POST', body: JSON.stringify(body) });
292
+ }
293
+ export async function requestJson(url, token, init) {
294
+ const response = await fetch(url, {
295
+ method: init.method,
296
+ redirect: 'error',
297
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
298
+ headers: {
299
+ 'Accept': 'application/json',
300
+ ...init.body === undefined ? {} : { 'Content-Type': 'application/json' },
301
+ ...token === undefined ? {} : { Authorization: `Auth ${token}` },
302
+ },
303
+ ...init.body === undefined ? {} : { body: init.body },
304
+ });
305
+ const text = await response.text();
306
+ if (!response.ok) {
307
+ let code;
308
+ let message = `HTTP ${response.status}`;
309
+ try {
310
+ const envelope = JSON.parse(text);
311
+ if (typeof envelope.error === 'object' && envelope.error !== null) {
312
+ code = typeof envelope.error.code === 'string' ? envelope.error.code : undefined;
313
+ if (typeof envelope.error.message === 'string')
314
+ message += `: ${envelope.error.message}`;
315
+ }
316
+ else if (typeof envelope.error === 'string') {
317
+ code = envelope.error;
318
+ if (typeof envelope.message === 'string')
319
+ message += `: ${envelope.message}`;
320
+ }
321
+ }
322
+ catch {
323
+ // A non-JSON error body carries nothing more.
324
+ }
325
+ throw new HttpError(response.status, code, code === undefined ? message : `${message} (${code})`, retryAfter(response.headers.get('Retry-After')));
326
+ }
327
+ return text === '' ? {} : JSON.parse(text);
328
+ }
329
+ export function retryAfter(header) {
330
+ if (header === null)
331
+ return undefined;
332
+ const seconds = Number(header);
333
+ if (Number.isFinite(seconds) && seconds >= 0)
334
+ return seconds * 1000;
335
+ const date = Date.parse(header);
336
+ return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
337
+ }
338
+ function base64url(bytes) {
339
+ return bytes.toString('base64url');
340
+ }
341
+ function operatingSystem() {
342
+ const name = { win32: 'Windows', darwin: 'macOS', linux: 'Linux' }[process.platform] ?? process.platform;
343
+ return `${name} ${os.release()}`.slice(0, 64);
344
+ }
345
+ function architecture() {
346
+ return process.arch === 'arm64' ? 'arm64' : 'x86_64';
347
+ }
@@ -0,0 +1 @@
1
+ export * from './account.js';
@@ -0,0 +1 @@
1
+ export * from './account.js';
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@openvulcan/openvulcan-account",
3
+ "version": "0.4.0",
4
+ "description": "OpenVulcan account: device sign-in, credential storage and refresh, shared by the Vulcan tools.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/OpenVulcan/dsh-vulcancode.git",
9
+ "directory": "packages/openvulcan-account"
10
+ },
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./lib/index.d.ts",
15
+ "default": "./lib/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "lib",
21
+ "!lib/**/*.test.*"
22
+ ],
23
+ "engines": {
24
+ "node": ">=20.19"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ }
29
+ }