@a5c-ai/kradle 5.0.1-staging.3abdf9534c25

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 (295) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +187 -0
  3. package/bin/kradle-demo.mjs +23 -0
  4. package/bin/kradle-server.mjs +14 -0
  5. package/dist/kradle-controller-ui.json +3482 -0
  6. package/dist/kradle-lifecycle.json +201 -0
  7. package/dist/kradle-runtime-snapshot.json +3125 -0
  8. package/dist/kradle-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/architecture-v2.md +2759 -0
  60. package/docs/components/control-plane.md +78 -0
  61. package/docs/components/data-plane.md +69 -0
  62. package/docs/components/hooks-events.md +67 -0
  63. package/docs/components/identity-rbac-policy.md +73 -0
  64. package/docs/components/kubevela-oam.md +70 -0
  65. package/docs/components/operations-publishing.md +81 -0
  66. package/docs/components/runners-ci.md +66 -0
  67. package/docs/components/web-ui.md +94 -0
  68. package/docs/crd-behaviors-and-relationships.md +3926 -0
  69. package/docs/external/README.md +47 -0
  70. package/docs/external/bidirectional-sync-design.md +134 -0
  71. package/docs/external/cicd-interface.md +64 -0
  72. package/docs/external/external-backend-controllers.md +170 -0
  73. package/docs/external/external-backend-crds.md +234 -0
  74. package/docs/external/external-backend-ui-spec.md +151 -0
  75. package/docs/external/external-backend-ux-flows.md +115 -0
  76. package/docs/external/external-object-mapping.md +125 -0
  77. package/docs/external/git-forge-interface.md +68 -0
  78. package/docs/external/github-integration-design.md +151 -0
  79. package/docs/external/issue-tracking-interface.md +66 -0
  80. package/docs/external/provider-capability-manifests.md +204 -0
  81. package/docs/external/provider-catalog.md +139 -0
  82. package/docs/external/provider-rollout-testing.md +78 -0
  83. package/docs/external/research-results.md +48 -0
  84. package/docs/external/security-auth-permissions.md +81 -0
  85. package/docs/external/sync-state-machines.md +108 -0
  86. package/docs/external/unified-external-backend-model.md +107 -0
  87. package/docs/external/user-facing-changes.md +67 -0
  88. package/docs/gaps.md +161 -0
  89. package/docs/install.md +94 -0
  90. package/docs/integration-and-design-decisions.md +1530 -0
  91. package/docs/kradle-design.md +334 -0
  92. package/docs/local-minikube.md +55 -0
  93. package/docs/ontology/README.md +32 -0
  94. package/docs/ontology/bounded-contexts.md +29 -0
  95. package/docs/ontology/events-and-hooks.md +32 -0
  96. package/docs/ontology/oam-kubevela.md +32 -0
  97. package/docs/ontology/operations-and-release.md +25 -0
  98. package/docs/ontology/personas-and-actors.md +32 -0
  99. package/docs/ontology/policies-and-invariants.md +33 -0
  100. package/docs/ontology/problem-space.md +30 -0
  101. package/docs/ontology/resource-contracts.md +40 -0
  102. package/docs/ontology/resource-taxonomy.md +42 -0
  103. package/docs/ontology/runners-and-ci.md +29 -0
  104. package/docs/ontology/solution-space.md +24 -0
  105. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  106. package/docs/ontology/validation-matrix.md +24 -0
  107. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  108. package/docs/ontology/workflows.md +39 -0
  109. package/docs/ontology/world.md +35 -0
  110. package/docs/openapi.yaml +1291 -0
  111. package/docs/product-requirements.md +62 -0
  112. package/docs/requirements-v2.md +235 -0
  113. package/docs/roadmap-mvp.md +87 -0
  114. package/docs/sdk-api-reference.md +1108 -0
  115. package/docs/system-requirements.md +90 -0
  116. package/docs/system-spec-v2.md +1230 -0
  117. package/docs/tests/README.md +53 -0
  118. package/docs/tests/agent-qa-plan.md +63 -0
  119. package/docs/tests/browser-ui-tests.md +62 -0
  120. package/docs/tests/ci-quality-gates.md +48 -0
  121. package/docs/tests/coverage-model.md +64 -0
  122. package/docs/tests/e2e-scenario-tests.md +53 -0
  123. package/docs/tests/fixtures-test-data.md +63 -0
  124. package/docs/tests/observability-reliability-tests.md +54 -0
  125. package/docs/tests/product-test-matrix.md +145 -0
  126. package/docs/tests/qa-adoption-roadmap.md +130 -0
  127. package/docs/tests/qa-automation-plan.md +101 -0
  128. package/docs/tests/security-compliance-tests.md +57 -0
  129. package/docs/tests/test-framework-tools.md +88 -0
  130. package/docs/tests/test-suite-layout.md +121 -0
  131. package/docs/tests/unit-integration-tests.md +48 -0
  132. package/docs/todo-kyverno +714 -0
  133. package/docs/todos.md +4 -0
  134. package/docs/user-stories.md +78 -0
  135. package/docs/web-console-spec.md +533 -0
  136. package/examples/minikube-demo.yaml +190 -0
  137. package/examples/oam-application.yaml +23 -0
  138. package/examples/policy-kyverno-pr-title.yaml +18 -0
  139. package/package.json +66 -0
  140. package/scripts/build.mjs +29 -0
  141. package/scripts/setup-minikube.mjs +65 -0
  142. package/scripts/smoke.mjs +37 -0
  143. package/scripts/validate-doc-coverage.mjs +152 -0
  144. package/scripts/validate-package.mjs +95 -0
  145. package/scripts/validate-ui.mjs +305 -0
  146. package/src/agent-adapter-controller.js +169 -0
  147. package/src/agent-approval-controller.js +170 -0
  148. package/src/agent-context-bundles.js +242 -0
  149. package/src/agent-dispatch-controller.js +549 -0
  150. package/src/agent-gateway-config-controller.js +147 -0
  151. package/src/agent-identity-migration.js +115 -0
  152. package/src/agent-memory-controller.js +357 -0
  153. package/src/agent-memory-import.js +327 -0
  154. package/src/agent-memory-query.js +292 -0
  155. package/src/agent-memory-repository-source-controller.js +255 -0
  156. package/src/agent-mux-client.js +589 -0
  157. package/src/agent-permission-review.js +250 -0
  158. package/src/agent-persona-controller.js +135 -0
  159. package/src/agent-project-controller.js +117 -0
  160. package/src/agent-prompt-composition.js +55 -0
  161. package/src/agent-provider-config-controller.js +151 -0
  162. package/src/agent-secret-config-grant-controller.js +282 -0
  163. package/src/agent-session-transcript-controller.js +189 -0
  164. package/src/agent-stack-controller.js +421 -0
  165. package/src/agent-subagent-controller.js +160 -0
  166. package/src/agent-transport-binding-controller.js +121 -0
  167. package/src/agent-trigger-controller.js +387 -0
  168. package/src/agent-workspace-controller.js +702 -0
  169. package/src/agent-writeback-controller.js +302 -0
  170. package/src/api-controller.js +621 -0
  171. package/src/argocd-gitops.js +43 -0
  172. package/src/artifact-registry-controller.js +542 -0
  173. package/src/assistant-runtime.js +284 -0
  174. package/src/async-controller.js +207 -0
  175. package/src/audit-controller.js +191 -0
  176. package/src/auth.js +310 -0
  177. package/src/component-catalog.js +41 -0
  178. package/src/control-plane.js +136 -0
  179. package/src/controller-client.js +112 -0
  180. package/src/controller-ui.js +620 -0
  181. package/src/data-plane.js +179 -0
  182. package/src/event-bus.js +397 -0
  183. package/src/external/conflict-controller.js +225 -0
  184. package/src/external/github/auth.js +96 -0
  185. package/src/external/github/cicd.js +180 -0
  186. package/src/external/github/git-forge.js +240 -0
  187. package/src/external/github/index.js +144 -0
  188. package/src/external/github/issue-tracking.js +163 -0
  189. package/src/external/provider-adapter.js +161 -0
  190. package/src/external/provider-resource-factory.js +221 -0
  191. package/src/external/sync-controller.js +235 -0
  192. package/src/external/webhook-controller.js +144 -0
  193. package/src/external/write-controller.js +283 -0
  194. package/src/gitea-backend.js +131 -0
  195. package/src/gitea-service.js +173 -0
  196. package/src/handoff.js +98 -0
  197. package/src/health-probes.js +134 -0
  198. package/src/hooks-events.js +63 -0
  199. package/src/hooks-lifecycle.js +117 -0
  200. package/src/http-server.js +409 -0
  201. package/src/identity-policy.js +86 -0
  202. package/src/index.js +71 -0
  203. package/src/jitsi-agent-bridge.js +141 -0
  204. package/src/jitsi-meeting-controller.js +291 -0
  205. package/src/jitsi-sync-controller.js +198 -0
  206. package/src/kradle-inference-service-controller.js +246 -0
  207. package/src/kubernetes-controller-async.js +531 -0
  208. package/src/kubernetes-controller.js +904 -0
  209. package/src/kubernetes-resource-gateway.js +48 -0
  210. package/src/model-route-controller.js +364 -0
  211. package/src/notification-controller.js +178 -0
  212. package/src/operations.js +112 -0
  213. package/src/org-scoping.js +5 -0
  214. package/src/resource-model.js +282 -0
  215. package/src/runner-controller.js +272 -0
  216. package/src/runners-ci.js +48 -0
  217. package/src/runtime.js +196 -0
  218. package/src/snapshot-cache.js +157 -0
  219. package/src/virtual-model-controller.js +538 -0
  220. package/src/virtual-model-hook-bridge.js +200 -0
  221. package/src/web-ui.js +40 -0
  222. package/tests/agent-adapter-controller.test.js +361 -0
  223. package/tests/agent-approval-controller.test.js +173 -0
  224. package/tests/agent-context-bundles.test.js +278 -0
  225. package/tests/agent-dispatch-controller.test.js +679 -0
  226. package/tests/agent-gateway-config-controller.test.js +386 -0
  227. package/tests/agent-identity-migration.test.js +87 -0
  228. package/tests/agent-memory-controller.test.js +461 -0
  229. package/tests/agent-memory-import-snapshot.test.js +477 -0
  230. package/tests/agent-memory-query.test.js +404 -0
  231. package/tests/agent-memory-repository-source.test.js +514 -0
  232. package/tests/agent-mux-client.test.js +389 -0
  233. package/tests/agent-mux-integration.test.js +971 -0
  234. package/tests/agent-permission-review-v2.test.js +317 -0
  235. package/tests/agent-permission-review.test.js +209 -0
  236. package/tests/agent-persona-controller.test.js +127 -0
  237. package/tests/agent-project-controller.test.js +302 -0
  238. package/tests/agent-prompt-composition.test.js +76 -0
  239. package/tests/agent-provider-config-controller.test.js +376 -0
  240. package/tests/agent-resources.test.js +303 -0
  241. package/tests/agent-secret-config-grant.test.js +231 -0
  242. package/tests/agent-session-transcript-controller.test.js +499 -0
  243. package/tests/agent-stack-controller.test.js +283 -0
  244. package/tests/agent-subagent-controller.test.js +201 -0
  245. package/tests/agent-transport-binding-controller.test.js +294 -0
  246. package/tests/agent-trigger-controller.test.js +271 -0
  247. package/tests/agent-trigger-routes.test.js +190 -0
  248. package/tests/agent-trigger-sources.test.js +245 -0
  249. package/tests/agent-workspace-controller.test.js +181 -0
  250. package/tests/agent-writeback.test.js +292 -0
  251. package/tests/approval-persistence.test.js +171 -0
  252. package/tests/artifact-registry.test.js +511 -0
  253. package/tests/assistant-runtime.test.js +506 -0
  254. package/tests/async-controller.test.js +252 -0
  255. package/tests/audit-controller.test.js +227 -0
  256. package/tests/codespace-controller.test.js +318 -0
  257. package/tests/controller-client.test.js +133 -0
  258. package/tests/deployment.test.js +527 -0
  259. package/tests/e2e/lifecycle.test.js +120 -0
  260. package/tests/event-bus-integration.test.js +355 -0
  261. package/tests/external-github-forge.test.js +560 -0
  262. package/tests/external-github-issues-cicd.test.js +520 -0
  263. package/tests/external-integration.test.js +470 -0
  264. package/tests/external-persistence.test.js +415 -0
  265. package/tests/external-provider-adapter.test.js +365 -0
  266. package/tests/external-resource-model.test.js +223 -0
  267. package/tests/external-webhook-sync.test.js +287 -0
  268. package/tests/external-write-conflict.test.js +353 -0
  269. package/tests/gitea-service.test.js +253 -0
  270. package/tests/health-check-real.test.js +165 -0
  271. package/tests/health-probes.test.js +90 -0
  272. package/tests/hooks-lifecycle.test.js +364 -0
  273. package/tests/integration/full-flow.test.js +266 -0
  274. package/tests/jitsi-agent-bridge.test.js +119 -0
  275. package/tests/jitsi-helm-integration.test.js +77 -0
  276. package/tests/jitsi-meeting-controller.test.js +170 -0
  277. package/tests/jitsi-resource-model.test.js +73 -0
  278. package/tests/jitsi-sync-controller.test.js +112 -0
  279. package/tests/kradle-inference-service.test.js +689 -0
  280. package/tests/kradle.test.js +779 -0
  281. package/tests/memory-search-wiring.test.js +270 -0
  282. package/tests/model-route-controller.test.js +733 -0
  283. package/tests/notification-controller.test.js +196 -0
  284. package/tests/notification-integration.test.js +179 -0
  285. package/tests/org-scoping.test.js +687 -0
  286. package/tests/runner-controller.test.js +327 -0
  287. package/tests/runner-integration.test.js +231 -0
  288. package/tests/session-cookie-hmac.test.js +151 -0
  289. package/tests/snapshot-performance.test.js +315 -0
  290. package/tests/sse-events.test.js +107 -0
  291. package/tests/virtual-model-controller.test.js +877 -0
  292. package/tests/virtual-model-hook-bridge.test.js +384 -0
  293. package/tests/webhook-trigger.test.js +198 -0
  294. package/tests/workspace-volumes.test.js +312 -0
  295. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,542 @@
1
+ // Artifact Registry Controller
2
+ // Manages ArtifactRegistry, ArtifactFeed, ArtifactAccessPolicy resources
3
+ // and artifact operations (publish, list, delete) for npm, pip, Docker, and generic registries.
4
+
5
+ import { createHash } from 'node:crypto';
6
+ import { createResource, clone } from './resource-model.js';
7
+
8
+ export const ARTIFACT_REGISTRY_CONTROLLER_BOUNDARY = {
9
+ role: 'artifact-registry-controller',
10
+ scope: 'Artifact registry lifecycle: validation, feed management, access policy, publish/list/delete operations for npm, pip, Docker, and generic artifact registries',
11
+ owns: ['registry validation', 'feed management', 'access policy enforcement', 'artifact publish', 'artifact version listing', 'artifact deletion', 'download audit'],
12
+ delegatesTo: ['resource-model', 'external-backend-binding'],
13
+ mustNotOwn: ['secret values', 'blob storage I/O', 'network transport']
14
+ };
15
+
16
+ const VALID_REGISTRY_TYPES = ['npm', 'pip', 'docker', 'generic'];
17
+ const VALID_STORAGE_BACKENDS = ['internal', 's3', 'azure-blob', 'gcs'];
18
+ const VALID_PERMISSIONS = ['read', 'write', 'admin'];
19
+ const VALID_EXTERNAL_MODES = ['read-only', 'read-write', 'mirror'];
20
+
21
+ function sha256(data) {
22
+ return createHash('sha256').update(typeof data === 'string' ? data : JSON.stringify(data)).digest('hex');
23
+ }
24
+
25
+ /**
26
+ * Create an artifact registry controller instance.
27
+ *
28
+ * @param {object} [options]
29
+ * @returns {object} controller
30
+ */
31
+ export function createArtifactRegistryController(options = {}) {
32
+ // In-memory stores for artifact versions and download audit records.
33
+ // In production these are backed by postgres; here we use arrays for
34
+ // unit-testable logic.
35
+ const versions = [];
36
+ const downloads = [];
37
+
38
+ return {
39
+ ...ARTIFACT_REGISTRY_CONTROLLER_BOUNDARY,
40
+
41
+ // -----------------------------------------------------------------------
42
+ // Registry validation
43
+ // -----------------------------------------------------------------------
44
+
45
+ /**
46
+ * Validate an ArtifactRegistry resource.
47
+ * @param {object} resource
48
+ * @returns {{ valid: boolean, errors: string[] }}
49
+ */
50
+ validateRegistry(resource) {
51
+ const errors = [];
52
+ if (resource == null) {
53
+ errors.push('resource must not be null or undefined');
54
+ return { valid: false, errors };
55
+ }
56
+ if (!resource.metadata?.name) {
57
+ errors.push('metadata.name is required');
58
+ }
59
+ const spec = resource.spec || {};
60
+ if (!spec.organizationRef) {
61
+ errors.push('spec.organizationRef is required');
62
+ }
63
+ if (!spec.registryType) {
64
+ errors.push(`spec.registryType is required; valid types are: ${VALID_REGISTRY_TYPES.join(', ')}`);
65
+ } else if (!VALID_REGISTRY_TYPES.includes(spec.registryType)) {
66
+ errors.push(`spec.registryType "${spec.registryType}" is not supported; valid types are: ${VALID_REGISTRY_TYPES.join(', ')}`);
67
+ }
68
+ if (!spec.storageBackend) {
69
+ errors.push(`spec.storageBackend is required; valid backends are: ${VALID_STORAGE_BACKENDS.join(', ')}`);
70
+ } else if (!VALID_STORAGE_BACKENDS.includes(spec.storageBackend)) {
71
+ errors.push(`spec.storageBackend "${spec.storageBackend}" is not supported; valid backends are: ${VALID_STORAGE_BACKENDS.join(', ')}`);
72
+ }
73
+ // External backend requires externalBackendRef
74
+ if (spec.storageBackend && spec.storageBackend !== 'internal' && spec.externalBackendRef) {
75
+ // valid — external backend ref present for non-internal storage
76
+ }
77
+ return { valid: errors.length === 0, errors };
78
+ },
79
+
80
+ // -----------------------------------------------------------------------
81
+ // Registry endpoint resolution
82
+ // -----------------------------------------------------------------------
83
+
84
+ /**
85
+ * Return the registry endpoint URL for a given registry resource.
86
+ * Internal registries use the kradle API URL pattern; external registries
87
+ * resolve to their provider-specific endpoints.
88
+ *
89
+ * @param {object} registry - ArtifactRegistry resource
90
+ * @param {object} [opts]
91
+ * @param {string} [opts.baseUrl] - Kradle API base URL (default: https://kradle.example.com)
92
+ * @returns {string} endpoint URL
93
+ */
94
+ getRegistryEndpoint(registry, opts = {}) {
95
+ const spec = registry?.spec || {};
96
+ const baseUrl = opts.baseUrl || 'https://kradle.example.com';
97
+ const org = spec.organizationRef || 'default';
98
+
99
+ // External backends
100
+ if (spec.externalBackendRef) {
101
+ const providerType = spec.externalProvider || spec.registryType;
102
+ if (providerType === 'npm' || spec.externalUrl?.includes('npm.pkg.github.com')) {
103
+ return spec.externalUrl || `https://npm.pkg.github.com/@${org}`;
104
+ }
105
+ if (providerType === 'docker' || spec.externalUrl?.includes('ghcr.io')) {
106
+ return spec.externalUrl || `https://ghcr.io/${org}`;
107
+ }
108
+ if (providerType === 'pip') {
109
+ return spec.externalUrl || 'https://pypi.org/simple/';
110
+ }
111
+ // Generic external
112
+ return spec.externalUrl || `${baseUrl}/api/v1/registry/${spec.registryType}/${org}`;
113
+ }
114
+
115
+ // Internal registry
116
+ return `${baseUrl}/api/v1/registry/${spec.registryType}/${org}`;
117
+ },
118
+
119
+ // -----------------------------------------------------------------------
120
+ // Feed validation
121
+ // -----------------------------------------------------------------------
122
+
123
+ /**
124
+ * Validate an ArtifactFeed resource.
125
+ * @param {object} resource
126
+ * @returns {{ valid: boolean, errors: string[] }}
127
+ */
128
+ validateFeed(resource) {
129
+ const errors = [];
130
+ if (resource == null) {
131
+ errors.push('resource must not be null or undefined');
132
+ return { valid: false, errors };
133
+ }
134
+ if (!resource.metadata?.name) {
135
+ errors.push('metadata.name is required');
136
+ }
137
+ const spec = resource.spec || {};
138
+ if (!spec.organizationRef) {
139
+ errors.push('spec.organizationRef is required');
140
+ }
141
+ if (!spec.registryRef) {
142
+ errors.push('spec.registryRef is required');
143
+ }
144
+ if (!spec.feedName) {
145
+ errors.push('spec.feedName is required');
146
+ }
147
+ return { valid: errors.length === 0, errors };
148
+ },
149
+
150
+ // -----------------------------------------------------------------------
151
+ // Access policy validation
152
+ // -----------------------------------------------------------------------
153
+
154
+ /**
155
+ * Validate an ArtifactAccessPolicy resource.
156
+ * @param {object} resource
157
+ * @returns {{ valid: boolean, errors: string[] }}
158
+ */
159
+ validateAccessPolicy(resource) {
160
+ const errors = [];
161
+ if (resource == null) {
162
+ errors.push('resource must not be null or undefined');
163
+ return { valid: false, errors };
164
+ }
165
+ if (!resource.metadata?.name) {
166
+ errors.push('metadata.name is required');
167
+ }
168
+ const spec = resource.spec || {};
169
+ if (!spec.organizationRef) {
170
+ errors.push('spec.organizationRef is required');
171
+ }
172
+ if (!spec.feedRef) {
173
+ errors.push('spec.feedRef is required');
174
+ }
175
+ if (!Array.isArray(spec.subjects) || spec.subjects.length === 0) {
176
+ errors.push('spec.subjects must be a non-empty array');
177
+ }
178
+ if (!Array.isArray(spec.permissions) || spec.permissions.length === 0) {
179
+ errors.push(`spec.permissions must be a non-empty array; valid permissions are: ${VALID_PERMISSIONS.join(', ')}`);
180
+ } else {
181
+ const unknown = spec.permissions.filter((p) => !VALID_PERMISSIONS.includes(p));
182
+ if (unknown.length > 0) {
183
+ errors.push(`spec.permissions contains unknown permission(s): ${unknown.join(', ')}; valid permissions are: ${VALID_PERMISSIONS.join(', ')}`);
184
+ }
185
+ }
186
+ return { valid: errors.length === 0, errors };
187
+ },
188
+
189
+ // -----------------------------------------------------------------------
190
+ // Access policy enforcement
191
+ // -----------------------------------------------------------------------
192
+
193
+ /**
194
+ * Check whether a subject has the required permission on a feed.
195
+ * @param {object} policy - ArtifactAccessPolicy resource
196
+ * @param {string} subject - subject identifier (user, team, or service account name)
197
+ * @param {string} requiredPermission - 'read' | 'write' | 'admin'
198
+ * @returns {boolean}
199
+ */
200
+ checkAccess(policy, subject, requiredPermission) {
201
+ const spec = policy?.spec || {};
202
+ const subjects = spec.subjects || [];
203
+ const permissions = spec.permissions || [];
204
+ if (!subjects.includes(subject)) return false;
205
+ // admin implies write; write implies read
206
+ if (requiredPermission === 'read') {
207
+ return permissions.includes('read') || permissions.includes('write') || permissions.includes('admin');
208
+ }
209
+ if (requiredPermission === 'write') {
210
+ return permissions.includes('write') || permissions.includes('admin');
211
+ }
212
+ return permissions.includes('admin');
213
+ },
214
+
215
+ // -----------------------------------------------------------------------
216
+ // Artifact operations
217
+ // -----------------------------------------------------------------------
218
+
219
+ /**
220
+ * Publish an artifact version to a feed.
221
+ * Creates an ArtifactVersion resource with digest, size, and metadata.
222
+ *
223
+ * @param {object} feed - ArtifactFeed resource
224
+ * @param {object} artifactSpec - { name, version, digest?, size?, metadata? }
225
+ * @param {object} [opts] - { namespace, organizationRef }
226
+ * @returns {{ version: object, digest: string, publishedAt: string }}
227
+ */
228
+ async publishArtifact(feed, artifactSpec, opts = {}) {
229
+ const now = new Date().toISOString();
230
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName || 'unknown-feed';
231
+ const organizationRef = opts.organizationRef || feed?.spec?.organizationRef || 'default';
232
+ const namespace = opts.namespace || feed?.metadata?.namespace || 'default';
233
+ const digest = artifactSpec.digest || sha256(`${artifactSpec.name}@${artifactSpec.version}@${now}`);
234
+ const size = artifactSpec.size || 0;
235
+
236
+ const versionName = `artver-${sha256(feedRef + artifactSpec.name + artifactSpec.version + now).slice(0, 12)}`;
237
+ const versionResource = createResource('ArtifactVersion', { name: versionName, namespace }, {
238
+ organizationRef,
239
+ feedRef,
240
+ name: artifactSpec.name,
241
+ version: artifactSpec.version,
242
+ digest,
243
+ size,
244
+ metadata: artifactSpec.metadata || {},
245
+ });
246
+ versionResource.status = { phase: 'Published', publishedAt: now };
247
+ versions.push(versionResource);
248
+
249
+ return { version: versionResource, digest, publishedAt: now };
250
+ },
251
+
252
+ /**
253
+ * List artifact versions for a feed with optional pagination and filtering.
254
+ *
255
+ * @param {object} feed - ArtifactFeed resource
256
+ * @param {object} [opts] - { name?, limit?, offset? }
257
+ * @returns {{ items: object[], total: number }}
258
+ */
259
+ async listVersions(feed, opts = {}) {
260
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName;
261
+ let items = versions.filter((v) => v.spec.feedRef === feedRef);
262
+ if (opts.name) {
263
+ items = items.filter((v) => v.spec.name === opts.name);
264
+ }
265
+ const total = items.length;
266
+ const offset = opts.offset || 0;
267
+ const limit = opts.limit || 50;
268
+ items = items.slice(offset, offset + limit);
269
+ return { items: items.map(clone), total };
270
+ },
271
+
272
+ /**
273
+ * Get a specific artifact version by name and version string.
274
+ *
275
+ * @param {object} feed - ArtifactFeed resource
276
+ * @param {string} name - artifact name
277
+ * @param {string} version - version string
278
+ * @returns {object|null}
279
+ */
280
+ async getVersion(feed, name, version) {
281
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName;
282
+ const found = versions.find((v) =>
283
+ v.spec.feedRef === feedRef &&
284
+ v.spec.name === name &&
285
+ v.spec.version === version &&
286
+ v.status?.phase !== 'Deleted'
287
+ );
288
+ return found ? clone(found) : null;
289
+ },
290
+
291
+ /**
292
+ * Soft-delete an artifact version (retention policy applies).
293
+ *
294
+ * @param {object} feed - ArtifactFeed resource
295
+ * @param {string} name - artifact name
296
+ * @param {string} version - version string
297
+ * @returns {{ deleted: boolean, deletedAt?: string }}
298
+ */
299
+ async deleteVersion(feed, name, version) {
300
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName;
301
+ const found = versions.find((v) =>
302
+ v.spec.feedRef === feedRef &&
303
+ v.spec.name === name &&
304
+ v.spec.version === version
305
+ );
306
+ if (!found) return { deleted: false };
307
+ found.status = { ...found.status, phase: 'Deleted', deletedAt: new Date().toISOString() };
308
+ return { deleted: true, deletedAt: found.status.deletedAt };
309
+ },
310
+
311
+ // -----------------------------------------------------------------------
312
+ // Docker-specific operations
313
+ // -----------------------------------------------------------------------
314
+
315
+ /**
316
+ * List Docker tags for a repository within a feed.
317
+ * @param {object} feed
318
+ * @param {string} repository
319
+ * @returns {{ tags: string[] }}
320
+ */
321
+ async listDockerTags(feed, repository) {
322
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName;
323
+ const items = versions.filter((v) =>
324
+ v.spec.feedRef === feedRef &&
325
+ v.spec.name === repository &&
326
+ v.status?.phase !== 'Deleted'
327
+ );
328
+ return { tags: items.map((v) => v.spec.version) };
329
+ },
330
+
331
+ /**
332
+ * Get a Docker manifest for a repository reference.
333
+ * @param {object} feed
334
+ * @param {string} repository
335
+ * @param {string} reference - tag or digest
336
+ * @returns {object|null}
337
+ */
338
+ async getDockerManifest(feed, repository, reference) {
339
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName;
340
+ const found = versions.find((v) =>
341
+ v.spec.feedRef === feedRef &&
342
+ v.spec.name === repository &&
343
+ (v.spec.version === reference || v.spec.digest === reference) &&
344
+ v.status?.phase !== 'Deleted'
345
+ );
346
+ if (!found) return null;
347
+ return {
348
+ repository,
349
+ tag: found.spec.version,
350
+ digest: found.spec.digest,
351
+ size: found.spec.size || 0,
352
+ metadata: found.spec.metadata || {},
353
+ };
354
+ },
355
+
356
+ // -----------------------------------------------------------------------
357
+ // npm-specific operations
358
+ // -----------------------------------------------------------------------
359
+
360
+ /**
361
+ * Get npm package info (all non-deleted versions).
362
+ * @param {object} feed
363
+ * @param {string} packageName
364
+ * @returns {object|null}
365
+ */
366
+ async getNpmPackageInfo(feed, packageName) {
367
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName;
368
+ const items = versions.filter((v) =>
369
+ v.spec.feedRef === feedRef &&
370
+ v.spec.name === packageName &&
371
+ v.status?.phase !== 'Deleted'
372
+ );
373
+ if (items.length === 0) return null;
374
+ return {
375
+ name: packageName,
376
+ versions: Object.fromEntries(items.map((v) => [v.spec.version, {
377
+ digest: v.spec.digest,
378
+ size: v.spec.size || 0,
379
+ publishedAt: v.status?.publishedAt,
380
+ metadata: v.spec.metadata || {},
381
+ }])),
382
+ latestVersion: items[items.length - 1].spec.version,
383
+ };
384
+ },
385
+
386
+ /**
387
+ * List npm package versions.
388
+ * @param {object} feed
389
+ * @param {string} packageName
390
+ * @returns {{ versions: string[] }}
391
+ */
392
+ async listNpmVersions(feed, packageName) {
393
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName;
394
+ const items = versions.filter((v) =>
395
+ v.spec.feedRef === feedRef &&
396
+ v.spec.name === packageName &&
397
+ v.status?.phase !== 'Deleted'
398
+ );
399
+ return { versions: items.map((v) => v.spec.version) };
400
+ },
401
+
402
+ // -----------------------------------------------------------------------
403
+ // pip-specific operations
404
+ // -----------------------------------------------------------------------
405
+
406
+ /**
407
+ * List pip packages in a feed.
408
+ * @param {object} feed
409
+ * @returns {{ packages: string[] }}
410
+ */
411
+ async listPipPackages(feed) {
412
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName;
413
+ const items = versions.filter((v) =>
414
+ v.spec.feedRef === feedRef &&
415
+ v.status?.phase !== 'Deleted'
416
+ );
417
+ return { packages: [...new Set(items.map((v) => v.spec.name))] };
418
+ },
419
+
420
+ /**
421
+ * Get pip package info.
422
+ * @param {object} feed
423
+ * @param {string} packageName
424
+ * @returns {object|null}
425
+ */
426
+ async getPipPackageInfo(feed, packageName) {
427
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName;
428
+ const items = versions.filter((v) =>
429
+ v.spec.feedRef === feedRef &&
430
+ v.spec.name === packageName &&
431
+ v.status?.phase !== 'Deleted'
432
+ );
433
+ if (items.length === 0) return null;
434
+ return {
435
+ name: packageName,
436
+ versions: items.map((v) => ({
437
+ version: v.spec.version,
438
+ digest: v.spec.digest,
439
+ size: v.spec.size || 0,
440
+ })),
441
+ };
442
+ },
443
+
444
+ // -----------------------------------------------------------------------
445
+ // Generic/ad-hoc artifact operations
446
+ // -----------------------------------------------------------------------
447
+
448
+ /**
449
+ * Upload a generic artifact (creates a version from file metadata).
450
+ * @param {object} feed
451
+ * @param {object} file - { name, size, contentType? }
452
+ * @param {object} [metadata]
453
+ * @param {object} [opts]
454
+ * @returns {{ version: object, digest: string, publishedAt: string }}
455
+ */
456
+ async uploadArtifact(feed, file, metadata = {}, opts = {}) {
457
+ const now = new Date().toISOString();
458
+ const digest = sha256(`${file.name}@${file.size}@${now}`);
459
+ return this.publishArtifact(feed, {
460
+ name: file.name,
461
+ version: metadata.version || `${Date.now()}`,
462
+ digest,
463
+ size: file.size || 0,
464
+ metadata: { contentType: file.contentType || 'application/octet-stream', ...metadata },
465
+ }, opts);
466
+ },
467
+
468
+ /**
469
+ * Record a download audit event.
470
+ * @param {object} feed
471
+ * @param {string} artifactName
472
+ * @param {string} version
473
+ * @param {string} requestedBy
474
+ * @param {object} [opts]
475
+ * @returns {object} ArtifactDownload resource
476
+ */
477
+ async recordDownload(feed, artifactName, version, requestedBy, opts = {}) {
478
+ const now = new Date().toISOString();
479
+ const feedRef = feed?.metadata?.name || feed?.spec?.feedName || 'unknown-feed';
480
+ const organizationRef = opts.organizationRef || feed?.spec?.organizationRef || 'default';
481
+ const namespace = opts.namespace || feed?.metadata?.namespace || 'default';
482
+ const artifactRef = `${feedRef}/${artifactName}@${version}`;
483
+ const dlName = `artdl-${sha256(artifactRef + requestedBy + now).slice(0, 12)}`;
484
+
485
+ const dlResource = createResource('ArtifactDownload', { name: dlName, namespace }, {
486
+ organizationRef,
487
+ artifactRef,
488
+ requestedBy,
489
+ feedRef,
490
+ artifactName,
491
+ version,
492
+ });
493
+ dlResource.status = { phase: 'Completed', downloadedAt: now };
494
+ downloads.push(dlResource);
495
+ return dlResource;
496
+ },
497
+
498
+ /**
499
+ * Download an artifact (returns version metadata + records audit).
500
+ * @param {object} feed
501
+ * @param {string} name
502
+ * @param {string} version
503
+ * @param {string} requestedBy
504
+ * @param {object} [opts]
505
+ * @returns {{ artifact: object|null, download: object|null }}
506
+ */
507
+ async downloadArtifact(feed, name, version, requestedBy, opts = {}) {
508
+ const artifact = await this.getVersion(feed, name, version);
509
+ if (!artifact) return { artifact: null, download: null };
510
+ const download = await this.recordDownload(feed, name, version, requestedBy, opts);
511
+ return { artifact, download };
512
+ },
513
+
514
+ // -----------------------------------------------------------------------
515
+ // External registry integration helper
516
+ // -----------------------------------------------------------------------
517
+
518
+ /**
519
+ * Resolve external registry capabilities from an ExternalBackendBinding.
520
+ * Returns the artifact registry interface configuration if enabled.
521
+ *
522
+ * @param {object} binding - ExternalBackendBinding resource
523
+ * @returns {{ enabled: boolean, mode: string }|null}
524
+ */
525
+ resolveExternalRegistryCapability(binding) {
526
+ const interfaces = binding?.spec?.interfaces;
527
+ if (!interfaces) return null;
528
+ const artifactRegistry = interfaces.artifactRegistry;
529
+ if (!artifactRegistry || !artifactRegistry.enabled) return null;
530
+ const mode = artifactRegistry.mode;
531
+ if (mode && !VALID_EXTERNAL_MODES.includes(mode)) return null;
532
+ return { enabled: true, mode: mode || 'read-only' };
533
+ },
534
+
535
+ // -----------------------------------------------------------------------
536
+ // Internal accessors (for testing)
537
+ // -----------------------------------------------------------------------
538
+
539
+ _versions: versions,
540
+ _downloads: downloads,
541
+ };
542
+ }