@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.
- package/Dockerfile +31 -0
- package/README.md +187 -0
- package/bin/kradle-demo.mjs +23 -0
- package/bin/kradle-server.mjs +14 -0
- package/dist/kradle-controller-ui.json +3482 -0
- package/dist/kradle-lifecycle.json +201 -0
- package/dist/kradle-runtime-snapshot.json +3125 -0
- package/dist/kradle-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/architecture-v2.md +2759 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/crd-behaviors-and-relationships.md +3926 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/integration-and-design-decisions.md +1530 -0
- package/docs/kradle-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1291 -0
- package/docs/product-requirements.md +62 -0
- package/docs/requirements-v2.md +235 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/sdk-api-reference.md +1108 -0
- package/docs/system-requirements.md +90 -0
- package/docs/system-spec-v2.md +1230 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/docs/web-console-spec.md +533 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +66 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +95 -0
- package/scripts/validate-ui.mjs +305 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +549 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-identity-migration.js +115 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +589 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-persona-controller.js +135 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-prompt-composition.js +55 -0
- package/src/agent-provider-config-controller.js +151 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +421 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +387 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +621 -0
- package/src/argocd-gitops.js +43 -0
- package/src/artifact-registry-controller.js +542 -0
- package/src/assistant-runtime.js +284 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +310 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +112 -0
- package/src/controller-ui.js +620 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +397 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +221 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/health-probes.js +134 -0
- package/src/hooks-events.js +63 -0
- package/src/hooks-lifecycle.js +117 -0
- package/src/http-server.js +409 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +71 -0
- package/src/jitsi-agent-bridge.js +141 -0
- package/src/jitsi-meeting-controller.js +291 -0
- package/src/jitsi-sync-controller.js +198 -0
- package/src/kradle-inference-service-controller.js +246 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +904 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/model-route-controller.js +364 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +282 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/virtual-model-controller.js +538 -0
- package/src/virtual-model-hook-bridge.js +200 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +679 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-identity-migration.test.js +87 -0
- package/tests/agent-memory-controller.test.js +461 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +389 -0
- package/tests/agent-mux-integration.test.js +971 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-persona-controller.test.js +127 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-prompt-composition.test.js +76 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +303 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +283 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +271 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/artifact-registry.test.js +511 -0
- package/tests/assistant-runtime.test.js +506 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +527 -0
- package/tests/e2e/lifecycle.test.js +120 -0
- package/tests/event-bus-integration.test.js +355 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +415 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +223 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/health-probes.test.js +90 -0
- package/tests/hooks-lifecycle.test.js +364 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/jitsi-agent-bridge.test.js +119 -0
- package/tests/jitsi-helm-integration.test.js +77 -0
- package/tests/jitsi-meeting-controller.test.js +170 -0
- package/tests/jitsi-resource-model.test.js +73 -0
- package/tests/jitsi-sync-controller.test.js +112 -0
- package/tests/kradle-inference-service.test.js +689 -0
- package/tests/kradle.test.js +779 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/model-route-controller.test.js +733 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/virtual-model-controller.test.js +877 -0
- package/tests/virtual-model-hook-bridge.test.js +384 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- 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
|
+
}
|