@atmem/architect 0.0.0-stage → 0.2.0-alpha.4.1
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/CONSTITUTION.md +144 -0
- package/README.md +33 -2
- package/agent-skills/discovery/SKILL.md +48 -0
- package/agent-skills/discovery/topics.json +139 -0
- package/agent-skills/generation-capabilities.json +760 -0
- package/agent-skills/service-lifecycle.json +21 -0
- package/agent-skills/specialists/architecture/SKILL.md +15 -0
- package/agent-skills/specialists/aws/SKILL.md +17 -0
- package/agent-skills/specialists/azure/SKILL.md +7 -0
- package/agent-skills/specialists/composition/SKILL.md +17 -0
- package/agent-skills/specialists/devops/SKILL.md +10 -0
- package/agent-skills/specialists/finops/SKILL.md +7 -0
- package/agent-skills/specialists/gcp/SKILL.md +7 -0
- package/agent-skills/specialists/ibm/SKILL.md +7 -0
- package/agent-skills/specialists/manifest.json +13 -0
- package/agent-skills/specialists/pulumi/SKILL.md +7 -0
- package/agent-skills/specialists/security/SKILL.md +7 -0
- package/agent-skills/specialists/terraform/SKILL.md +14 -0
- package/agent-skills/upstream/aws/LICENSE +175 -0
- package/agent-skills/upstream/aws/SKILL.md +125 -0
- package/agent-skills/upstream/azure/LICENSE +21 -0
- package/agent-skills/upstream/azure/SKILL.md +387 -0
- package/agent-skills/upstream/gcp/LICENSE +202 -0
- package/agent-skills/upstream/gcp/SKILL.md +159 -0
- package/agent-skills/upstream/manifest.json +57 -0
- package/agent-skills/upstream/pulumi/LICENSE +201 -0
- package/agent-skills/upstream/pulumi/SKILL.md +501 -0
- package/agent-skills/upstream/terraform/LICENSE +373 -0
- package/agent-skills/upstream/terraform/SKILL.md +316 -0
- package/agent-skills/workflows/workflows.json +601 -0
- package/cli/architect.mjs +3 -0
- package/cli/runtime.mjs +115 -0
- package/dist/assets/html2canvas.esm-QH1iLAAe.js +22 -0
- package/dist/assets/index-B97MkVMs.css +1 -0
- package/dist/assets/index-qtCM2D7O.js +21271 -0
- package/dist/assets/index.es-C6C0sneB.js +18 -0
- package/dist/assets/purify.es-CAi2OJFm.js +3 -0
- package/dist/assets/visual-export-DjbjgQbA.js +173 -0
- package/dist/favicon.svg +2 -0
- package/dist/fonts/DM-Sans-OFL.txt +93 -0
- package/dist/fonts/Manrope-OFL.txt +93 -0
- package/dist/fonts/NotoSans-Regular.ttf +0 -0
- package/dist/fonts/OFL.txt +94 -0
- package/dist/fonts/dm-sans-latin-wght-normal.woff2 +0 -0
- package/dist/fonts/manrope-latin-wght-normal.woff2 +0 -0
- package/dist/index.html +4 -0
- package/dist/licenses/aws-architect-LICENSE.txt +17 -0
- package/dist/licenses/aws-mcp-LICENSE.txt +175 -0
- package/dist/licenses/aws-mcp-NOTICE.txt +1 -0
- package/dist/licenses/cloud-emulation-notices.txt +36619 -0
- package/dist/licenses/dependency-notices.txt +6379 -0
- package/dist/licenses/emulation-notices.txt +717 -0
- package/dist/licenses/github-copilot-sdk-LICENSE.txt +21 -0
- package/dist/licenses/memory-dependency-notices.txt +3319 -0
- package/dist/licenses/openarchflow-LICENSE.txt +21 -0
- package/dist/licenses/python-dependency-notices.txt +11951 -0
- package/docs/upstream-lock.json +26 -0
- package/knowledge/engineering-practices.json +536 -0
- package/package.json +30 -4
- package/packaging/uv-release-lock.json +54 -0
- package/public/licenses/aws-architect-LICENSE.txt +17 -0
- package/public/licenses/aws-mcp-LICENSE.txt +175 -0
- package/public/licenses/aws-mcp-NOTICE.txt +1 -0
- package/public/licenses/cloud-emulation-notices.txt +36619 -0
- package/public/licenses/dependency-notices.txt +6379 -0
- package/public/licenses/emulation-notices.txt +717 -0
- package/public/licenses/github-copilot-sdk-LICENSE.txt +21 -0
- package/public/licenses/memory-dependency-notices.txt +3319 -0
- package/public/licenses/openarchflow-LICENSE.txt +21 -0
- package/public/licenses/python-dependency-notices.txt +11951 -0
- package/scripts/terraform-render.mjs +8 -0
- package/server/__init__.py +0 -0
- package/server/abstraction.py +28 -0
- package/server/agent_runtime.py +153 -0
- package/server/app.py +257 -0
- package/server/architect.py +112 -0
- package/server/architecture_activity.py +67 -0
- package/server/attachments.py +101 -0
- package/server/cloud-emulation-requirements.txt +29 -0
- package/server/codex_app_server.py +219 -0
- package/server/codex_engineering_backend.py +266 -0
- package/server/codex_worker_policy.py +170 -0
- package/server/contracts.py +315 -0
- package/server/copilot_worker.py +16 -0
- package/server/dependency_mirror.py +54 -0
- package/server/design_patch.py +43 -0
- package/server/emulation-requirements.txt +8 -0
- package/server/emulation_adapters.py +20 -0
- package/server/emulation_routes.py +98 -0
- package/server/emulation_worker.py +351 -0
- package/server/engineering_artifacts.py +215 -0
- package/server/engineering_assets/application-runtime.json +4372 -0
- package/server/engineering_assets/architecture_work.schema.json +79 -0
- package/server/engineering_assets/materialize_runtime.py +62 -0
- package/server/engineering_assets/prepare_tools.py +96 -0
- package/server/engineering_assets/sql_validation.py +59 -0
- package/server/engineering_contexts.py +120 -0
- package/server/engineering_delivery.py +210 -0
- package/server/engineering_finalization.py +210 -0
- package/server/engineering_handoff.py +163 -0
- package/server/engineering_jobs.py +206 -0
- package/server/engineering_knowledge.py +75 -0
- package/server/engineering_reconciliation.py +159 -0
- package/server/engineering_routes.py +469 -0
- package/server/engineering_sessions.py +273 -0
- package/server/engineering_tools.py +166 -0
- package/server/generation_lint.py +18 -0
- package/server/local_providers.py +181 -0
- package/server/local_validation.py +236 -0
- package/server/memory-requirements.lock +181 -0
- package/server/memory-requirements.txt +6 -0
- package/server/memory_routes.py +82 -0
- package/server/memory_service.py +273 -0
- package/server/memory_worker.py +66 -0
- package/server/openai_voice.py +80 -0
- package/server/packaged_app.py +9 -0
- package/server/pricing.py +84 -0
- package/server/process_control_observation.py +31 -0
- package/server/prompts.py +123 -0
- package/server/public_faq_emulation_worker.py +44 -0
- package/server/requirements.lock +1618 -0
- package/server/requirements.txt +68 -0
- package/server/runtime_dependency_locks.py +27 -0
- package/server/runtime_evidence.py +93 -0
- package/server/settings.py +14 -0
- package/server/skills.py +103 -0
- package/server/storage_emulation_worker.py +160 -0
- package/server/structured_emulation_worker.py +48 -0
- package/server/terraform_routes.py +118 -0
- package/server/upstream/__init__.py +0 -0
- package/server/upstream/agent/__init__.py +1 -0
- package/server/upstream/agent/aws_architect.py +600 -0
- package/server/upstream/agent/config/__init__.py +1 -0
- package/server/upstream/agent/config/agent_config.py +82 -0
- package/server/upstream/agent/config/prompts.py +138 -0
- package/server/upstream/agent/rate_limiter.py +62 -0
- package/server/upstream/utils/__init__.py +1 -0
- package/server/upstream/utils/file_writer.py +43 -0
- package/server/validation_applicability.py +85 -0
- package/server/validation_jobs.py +252 -0
- package/server/validation_workers.py +48 -0
- package/server/voice_routes.py +91 -0
- package/server/voice_worker.py +26 -0
- package/specs/028-convergence-evaluation/evidence/20261110-ibm-invoice-native/registry-structured/qualification.json +72 -0
- package/specs/028-convergence-evaluation/evidence/20261110-invoice-runtime/durable-current/receipt.json +61 -0
- package/specs/029-general-purpose-engine/evidence/20261111-runtime-dependencies/sdk/receipt.json +22 -0
- package/src/ArchitectureActivity.jsx +54 -0
- package/src/ArchitectureWorkPanel.jsx +46 -0
- package/src/ArtifactChecks.jsx +49 -0
- package/src/BlockRequirements.jsx +13 -0
- package/src/BrainActivity.jsx +11 -0
- package/src/ChatMessage.jsx +19 -0
- package/src/ChatResizeHandle.jsx +6 -0
- package/src/CodePreview.jsx +8 -0
- package/src/ComponentReview.jsx +31 -0
- package/src/DeliveryViews.jsx +59 -0
- package/src/EmulationPanel.jsx +44 -0
- package/src/EngagementViews.jsx +385 -0
- package/src/EngineeringPanel.jsx +151 -0
- package/src/ImplementationDelivery.jsx +55 -0
- package/src/JourneyViews.jsx +400 -0
- package/src/MemoryPanel.jsx +46 -0
- package/src/OpenAIVoicePanel.jsx +65 -0
- package/src/OutputsView.jsx +65 -0
- package/src/TerraformView.jsx +57 -0
- package/src/WorkbenchDialogs.jsx +532 -0
- package/src/activity-presentation.mjs +28 -0
- package/src/ai-qualification-scenarios.mjs +31 -0
- package/src/approval-application-files.mjs +32 -0
- package/src/approval-applications.mjs +82 -0
- package/src/approval-spec-kit.mjs +30 -0
- package/src/architecture-activity.css +28 -0
- package/src/architecture-activity.mjs +63 -0
- package/src/architecture-work.mjs +176 -0
- package/src/artifact-work.mjs +103 -0
- package/src/async-documents.mjs +191 -0
- package/src/atflow.css +1140 -0
- package/src/atforma.css +748 -0
- package/src/atmem-tokens.css +79 -0
- package/src/aws-preflight.mjs +60 -0
- package/src/aws-public-faq.mjs +102 -0
- package/src/aws-validation-scenarios.mjs +10 -0
- package/src/aws-validation.mjs +48 -0
- package/src/azure-plan-mocks.mjs +25 -0
- package/src/azure-structured.mjs +133 -0
- package/src/bedrock-summary.mjs +152 -0
- package/src/bounded-rag.mjs +60 -0
- package/src/brand.css +9 -0
- package/src/canvas-view.mjs +28 -0
- package/src/capability-composition.mjs +44 -0
- package/src/chat-attachments.mjs +13 -0
- package/src/cloud-provider-locks.mjs +9 -0
- package/src/cloud-storage-scenarios.mjs +2 -0
- package/src/component-review.mjs +55 -0
- package/src/composition-spec-kit.mjs +30 -0
- package/src/consultation-context.mjs +15 -0
- package/src/consultation.mjs +52 -0
- package/src/container-contracts.mjs +5 -0
- package/src/container-inspection.mjs +75 -0
- package/src/container-runtime.mjs +30 -0
- package/src/container-security-inspection.mjs +49 -0
- package/src/container-terraform.mjs +109 -0
- package/src/conversation.mjs +17 -0
- package/src/core.mjs +863 -0
- package/src/cross-cloud-storage.mjs +100 -0
- package/src/delivery-capabilities.mjs +25 -0
- package/src/delivery.mjs +58 -0
- package/src/deployment-ownership.mjs +10 -0
- package/src/deployment-region.mjs +11 -0
- package/src/derive.mjs +31 -0
- package/src/design-build-flow.mjs +10 -0
- package/src/diagram-attention.mjs +24 -0
- package/src/discovery.mjs +359 -0
- package/src/document.mjs +170 -0
- package/src/emulation-feedback.mjs +61 -0
- package/src/emulation-guidance.mjs +37 -0
- package/src/engagement.mjs +274 -0
- package/src/engineering-client.mjs +73 -0
- package/src/engineering-defaults.mjs +17 -0
- package/src/engineering.css +31 -0
- package/src/enterprise-application.mjs +76 -0
- package/src/enterprise-aws-host-infrastructure.mjs +62 -0
- package/src/enterprise-aws-host-tests.mjs +15 -0
- package/src/enterprise-aws-infrastructure-tests.mjs +38 -0
- package/src/enterprise-aws-infrastructure.mjs +138 -0
- package/src/enterprise-google-infrastructure.mjs +84 -0
- package/src/enterprise-google-tests.mjs +25 -0
- package/src/estimate.mjs +188 -0
- package/src/existing-references.mjs +18 -0
- package/src/external-identities.mjs +17 -0
- package/src/faq-lambda-build.mjs +28 -0
- package/src/gcp-structured.mjs +104 -0
- package/src/generation-provider.mjs +7 -0
- package/src/generation-quality.mjs +69 -0
- package/src/handoff.mjs +141 -0
- package/src/ibm-registry.mjs +12 -0
- package/src/ibm-structured.mjs +103 -0
- package/src/identifiers.mjs +15 -0
- package/src/implementation-map.mjs +14 -0
- package/src/implementation-outcome.mjs +8 -0
- package/src/incomplete-terraform.mjs +9 -0
- package/src/integrations.ts +137 -0
- package/src/interface-contracts.mjs +30 -0
- package/src/invoice-application.mjs +66 -0
- package/src/invoice-aws-operations.mjs +136 -0
- package/src/invoice-aws.mjs +143 -0
- package/src/invoice-azure-operations.mjs +147 -0
- package/src/invoice-azure.mjs +134 -0
- package/src/invoice-builder.mjs +35 -0
- package/src/invoice-dependencies.mjs +43 -0
- package/src/invoice-gcp-operations.mjs +178 -0
- package/src/invoice-gcp.mjs +104 -0
- package/src/invoice-ibm-operations.mjs +278 -0
- package/src/invoice-ibm.mjs +142 -0
- package/src/invoice-operations.mjs +143 -0
- package/src/invoice-plan.mjs +45 -0
- package/src/invoice-region.mjs +6 -0
- package/src/invoice-spec-kit.mjs +61 -0
- package/src/invoice-state.mjs +7 -0
- package/src/invoice-terraform.mjs +29 -0
- package/src/invoice-tests.mjs +122 -0
- package/src/journey-workspace.css +72 -0
- package/src/journey.mjs +87 -0
- package/src/levels.mjs +145 -0
- package/src/local-agent.mjs +184 -0
- package/src/main.jsx +2458 -0
- package/src/model-policy-preflight.mjs +34 -0
- package/src/openai-voice.mjs +102 -0
- package/src/output-bundle.mjs +11 -0
- package/src/ownership-inspection.mjs +28 -0
- package/src/pack.mjs +201 -0
- package/src/pricing.mjs +31 -0
- package/src/production-state.mjs +8 -0
- package/src/project-copy.mjs +10 -0
- package/src/project-file.mjs +160 -0
- package/src/project-links.mjs +3 -0
- package/src/protected-state.mjs +43 -0
- package/src/public-faq-builder.mjs +19 -0
- package/src/public-faq-clouds.mjs +251 -0
- package/src/public-faq.mjs +122 -0
- package/src/pulumi.mjs +123 -0
- package/src/python-container.mjs +25 -0
- package/src/python-runtime.mjs +50 -0
- package/src/rag-handler.mjs +43 -0
- package/src/rag-web-assets.mjs +52 -0
- package/src/rag-web.mjs +68 -0
- package/src/records.mjs +177 -0
- package/src/resources.mjs +163 -0
- package/src/review.mjs +252 -0
- package/src/runtime/EnterpriseDocument.java +89 -0
- package/src/runtime/approved_http_destination.py +72 -0
- package/src/runtime/aws-lambda-qualification-lock.json +7 -0
- package/src/runtime/aws-python-lock.json +82 -0
- package/src/runtime/aws-web-python-lock.json +887 -0
- package/src/runtime/aws_json_model.py +42 -0
- package/src/runtime/azure-python-lock.json +863 -0
- package/src/runtime/azure-sources.json +8 -0
- package/src/runtime/azure-test-lock.json +23 -0
- package/src/runtime/azure_acceptance.py +192 -0
- package/src/runtime/azure_structured.py +153 -0
- package/src/runtime/bounded_rag.py +126 -0
- package/src/runtime/catalog_admin.py +70 -0
- package/src/runtime/cloud-sql-proxy-lock.json +8 -0
- package/src/runtime/cloud-sql-proxy-security-review.json +30 -0
- package/src/runtime/connector_cli.py +50 -0
- package/src/runtime/connector_sync.py +70 -0
- package/src/runtime/durable-python-lock.json +247 -0
- package/src/runtime/durable_workflow.py +325 -0
- package/src/runtime/enterprise-application-manifest.json +110 -0
- package/src/runtime/enterprise-application-sources.json +266 -0
- package/src/runtime/enterprise-auth-python-lock.json +592 -0
- package/src/runtime/enterprise-aws-host-bootstrap-source.json +1 -0
- package/src/runtime/enterprise-aws-host-probe-source.json +1 -0
- package/src/runtime/enterprise-aws-task-template.json +235 -0
- package/src/runtime/enterprise-document-lock.json +18 -0
- package/src/runtime/enterprise-document.Dockerfile +15 -0
- package/src/runtime/enterprise-runtime-aws-python-lock.json +1115 -0
- package/src/runtime/enterprise-runtime-azure-python-lock.json +1106 -0
- package/src/runtime/enterprise-runtime-gcp-python-lock.json +1060 -0
- package/src/runtime/enterprise-runtime-ibm-python-lock.json +1096 -0
- package/src/runtime/enterprise-runtime-python-lock.json +1058 -0
- package/src/runtime/enterprise-runtime.Dockerfile +15 -0
- package/src/runtime/enterprise-web-app.js +1 -0
- package/src/runtime/enterprise-web-entry.js +105 -0
- package/src/runtime/enterprise-web-lock.json +29 -0
- package/src/runtime/enterprise-web-notices.txt +229 -0
- package/src/runtime/enterprise-web.css +4 -0
- package/src/runtime/enterprise-web.html +8 -0
- package/src/runtime/enterprise_audit.py +91 -0
- package/src/runtime/enterprise_audit_schema.sql +25 -0
- package/src/runtime/enterprise_auth_schema.sql +67 -0
- package/src/runtime/enterprise_auth_store.py +241 -0
- package/src/runtime/enterprise_aws_host_bootstrap.py +120 -0
- package/src/runtime/enterprise_aws_host_probe.py +105 -0
- package/src/runtime/enterprise_aws_hosts.py +136 -0
- package/src/runtime/enterprise_aws_jobs.py +300 -0
- package/src/runtime/enterprise_aws_objects.py +93 -0
- package/src/runtime/enterprise_aws_transfer.py +83 -0
- package/src/runtime/enterprise_azure_batch_api.py +128 -0
- package/src/runtime/enterprise_azure_host.py +599 -0
- package/src/runtime/enterprise_azure_jobs.py +294 -0
- package/src/runtime/enterprise_azure_objects.py +234 -0
- package/src/runtime/enterprise_azure_pool.py +161 -0
- package/src/runtime/enterprise_azure_transfer.py +122 -0
- package/src/runtime/enterprise_configuration.py +136 -0
- package/src/runtime/enterprise_document.py +116 -0
- package/src/runtime/enterprise_document_cli.py +32 -0
- package/src/runtime/enterprise_document_contract.py +54 -0
- package/src/runtime/enterprise_document_job.py +109 -0
- package/src/runtime/enterprise_document_jobs.py +122 -0
- package/src/runtime/enterprise_document_jobs_schema.sql +23 -0
- package/src/runtime/enterprise_extraction_cache.py +186 -0
- package/src/runtime/enterprise_extraction_schema.sql +17 -0
- package/src/runtime/enterprise_google_jobs.py +275 -0
- package/src/runtime/enterprise_google_transfer.py +85 -0
- package/src/runtime/enterprise_http.py +212 -0
- package/src/runtime/enterprise_identity.py +52 -0
- package/src/runtime/enterprise_knowledge.py +152 -0
- package/src/runtime/enterprise_models.py +94 -0
- package/src/runtime/enterprise_native.py +181 -0
- package/src/runtime/enterprise_oauth.py +132 -0
- package/src/runtime/enterprise_operator.py +160 -0
- package/src/runtime/enterprise_parser_service.py +136 -0
- package/src/runtime/enterprise_parser_transport.py +68 -0
- package/src/runtime/enterprise_queries.py +255 -0
- package/src/runtime/enterprise_queries_schema.sql +27 -0
- package/src/runtime/enterprise_query_service.py +111 -0
- package/src/runtime/enterprise_secrets.py +110 -0
- package/src/runtime/enterprise_sessions.py +95 -0
- package/src/runtime/enterprise_sources.py +260 -0
- package/src/runtime/enterprise_web.py +39 -0
- package/src/runtime/enterprise_worker.py +68 -0
- package/src/runtime/export_contract_acceptance.py +18 -0
- package/src/runtime/faq-output-contract.json +40 -0
- package/src/runtime/faq-web-app.js +41 -0
- package/src/runtime/faq-web.css +1 -0
- package/src/runtime/faq-web.html +6 -0
- package/src/runtime/gcp-sources.json +9 -0
- package/src/runtime/gcp_acceptance.py +110 -0
- package/src/runtime/gcp_structured.py +150 -0
- package/src/runtime/http_acceptance.py +39 -0
- package/src/runtime/iap-web-app.js +36 -0
- package/src/runtime/iap-web-source.json +1 -0
- package/src/runtime/ibm-python-lock.json +856 -0
- package/src/runtime/ibm-sources.json +9 -0
- package/src/runtime/ibm_acceptance.py +133 -0
- package/src/runtime/ibm_structured.py +113 -0
- package/src/runtime/invoice-azapi-provider-lock.json +7 -0
- package/src/runtime/invoice-ibm-provider-lock.json +7 -0
- package/src/runtime/invoice-parser-lock.json +1199 -0
- package/src/runtime/invoice-parser-python-lock.json +56 -0
- package/src/runtime/invoice-parser.Dockerfile +190 -0
- package/src/runtime/invoice-sources.json +58 -0
- package/src/runtime/invoice-test-sources.json +24 -0
- package/src/runtime/invoice-web.css +2 -0
- package/src/runtime/invoice-web.html +12 -0
- package/src/runtime/invoice-web.js +112 -0
- package/src/runtime/invoice_configuration.py +17 -0
- package/src/runtime/invoice_document.py +95 -0
- package/src/runtime/invoice_extraction.py +96 -0
- package/src/runtime/invoice_http.py +129 -0
- package/src/runtime/invoice_identity.py +5 -0
- package/src/runtime/invoice_maintenance.py +40 -0
- package/src/runtime/invoice_native.py +77 -0
- package/src/runtime/invoice_object_store.py +153 -0
- package/src/runtime/invoice_operations.py +108 -0
- package/src/runtime/invoice_operator.py +79 -0
- package/src/runtime/invoice_operator_core.py +39 -0
- package/src/runtime/invoice_operator_job.py +147 -0
- package/src/runtime/invoice_parser_cli.py +17 -0
- package/src/runtime/invoice_parser_identity.py +39 -0
- package/src/runtime/invoice_parser_service.py +89 -0
- package/src/runtime/invoice_parser_transport.py +89 -0
- package/src/runtime/invoice_processing.py +24 -0
- package/src/runtime/invoice_retention.py +16 -0
- package/src/runtime/invoice_runtime_secrets.py +43 -0
- package/src/runtime/invoice_stage.py +80 -0
- package/src/runtime/invoice_web.py +39 -0
- package/src/runtime/invoice_worker.py +50 -0
- package/src/runtime/lambda_faq.py +5 -0
- package/src/runtime/parser-requirements.lock +4 -0
- package/src/runtime/policy_review.py +18 -0
- package/src/runtime/postgres-qualification-lock.json +21 -0
- package/src/runtime/public-faq-sources.json +22 -0
- package/src/runtime/public_catalog_admin.py +122 -0
- package/src/runtime/public_catalog_store.py +125 -0
- package/src/runtime/public_faq.py +144 -0
- package/src/runtime/public_faq_app.py +49 -0
- package/src/runtime/python-container-lock.json +18 -0
- package/src/runtime/rag-sources.json +10 -0
- package/src/runtime/rag_acceptance.py +60 -0
- package/src/runtime/rag_aws_adapter.py +95 -0
- package/src/runtime/rds-ca-source.json +9 -0
- package/src/runtime/rds-global-ca.pem +2888 -0
- package/src/runtime/state-contract-check-source.json +4 -0
- package/src/runtime/state_contract_check.py +338 -0
- package/src/runtime/structured-source.json +1 -0
- package/src/runtime/structured-web-app.js +71 -0
- package/src/runtime/structured-web-source.json +1 -0
- package/src/runtime/structured_http.py +105 -0
- package/src/runtime/structured_output.py +48 -0
- package/src/runtime/web-python-lock.json +802 -0
- package/src/runtime/web_user_admin.py +44 -0
- package/src/runtime/workflow_application.py +62 -0
- package/src/runtime/workflow_bindings.py +128 -0
- package/src/runtime/workflow_configuration.py +40 -0
- package/src/runtime/workflow_connection.py +105 -0
- package/src/runtime/workflow_contract.py +54 -0
- package/src/runtime/workflow_database.py +94 -0
- package/src/runtime/workflow_errors.py +6 -0
- package/src/runtime/workflow_http.py +178 -0
- package/src/runtime/workflow_identity.py +69 -0
- package/src/runtime/workflow_payload.py +118 -0
- package/src/runtime/workflow_process_stage.py +88 -0
- package/src/runtime/workflow_profile.py +91 -0
- package/src/runtime/workflow_review.py +106 -0
- package/src/runtime/workflow_role_bootstrap.py +104 -0
- package/src/runtime/workflow_schema.sql +72 -0
- package/src/runtime/workflow_worker.py +142 -0
- package/src/runtime-settings.mjs +15 -0
- package/src/s3-access.mjs +14 -0
- package/src/source-contracts.mjs +17 -0
- package/src/spec-kit.mjs +186 -0
- package/src/state-custody.mjs +32 -0
- package/src/state-inspection.mjs +45 -0
- package/src/state-spec-kit.mjs +13 -0
- package/src/storage-tests.mjs +25 -0
- package/src/structured-api.mjs +133 -0
- package/src/structured-contract.mjs +19 -0
- package/src/structured-operation-policy.mjs +2 -0
- package/src/structured-web.mjs +17 -0
- package/src/style.css +1 -0
- package/src/system-description.mjs +26 -0
- package/src/terraform-admission.mjs +66 -0
- package/src/terraform-composition.mjs +83 -0
- package/src/terraform-expressions.mjs +46 -0
- package/src/terraform-validation-coverage.mjs +11 -0
- package/src/terraform.mjs +128 -0
- package/src/text-statistics.mjs +51 -0
- package/src/upstream/openarchflow/format-payload.ts +26 -0
- package/src/upstream/openarchflow/import/mermaid.ts +205 -0
- package/src/upstream/openarchflow/import/sql-ddl.ts +109 -0
- package/src/upstream/openarchflow/layout.ts +54 -0
- package/src/upstream/openarchflow/providers/aws.ts +196 -0
- package/src/upstream/openarchflow/providers/azure.ts +126 -0
- package/src/upstream/openarchflow/providers/cloudnative.ts +71 -0
- package/src/upstream/openarchflow/providers/generic.ts +227 -0
- package/src/upstream/openarchflow/providers/integration.ts +22 -0
- package/src/upstream/openarchflow/providers/types.ts +2 -0
- package/src/upstream/openarchflow/templates.ts +937 -0
- package/src/upstream-catalog.mjs +10 -0
- package/src/useChatScroll.jsx +36 -0
- package/src/useEmulation.jsx +36 -0
- package/src/validation-receipt.mjs +10 -0
- package/src/visual-export.mjs +92 -0
- package/src/voice-conversation.mjs +117 -0
- package/src/work-intake.mjs +24 -0
- package/src/workflow-runtime-inventory.mjs +3 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Fresh installation only. Queue and domain state share one transaction.
|
|
2
|
+
CREATE TABLE enterprise_queries.schema_version(
|
|
3
|
+
version integer PRIMARY KEY CHECK(version=1),source_sha256 text NOT NULL,queue_version text NOT NULL
|
|
4
|
+
);
|
|
5
|
+
CREATE TABLE enterprise_queries.requests(
|
|
6
|
+
id uuid PRIMARY KEY,account_id text NOT NULL,principal_key text NOT NULL,
|
|
7
|
+
session_sha256 text NOT NULL,request_key uuid NOT NULL,
|
|
8
|
+
question_mac text NOT NULL,question_ciphertext text,
|
|
9
|
+
state text NOT NULL CHECK(state IN('queued','running','ready','failed','cancelled','expired')),
|
|
10
|
+
attempt integer NOT NULL DEFAULT 0 CHECK(attempt BETWEEN 0 AND 3),
|
|
11
|
+
lease_token uuid,lease_until timestamptz,result_ciphertext text,
|
|
12
|
+
error_code text CHECK(error_code IN('source','setup','signin','unavailable','answer')),
|
|
13
|
+
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
|
|
14
|
+
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),expires_at timestamptz NOT NULL,
|
|
15
|
+
UNIQUE(account_id,principal_key,session_sha256,request_key),
|
|
16
|
+
FOREIGN KEY(account_id,principal_key)REFERENCES enterprise_auth.memberships,
|
|
17
|
+
CHECK((state='running')=(lease_token IS NOT NULL AND lease_until IS NOT NULL)),
|
|
18
|
+
CHECK((state='ready')=(result_ciphertext IS NOT NULL))
|
|
19
|
+
);
|
|
20
|
+
CREATE INDEX enterprise_queries_requests_recovery ON enterprise_queries.requests(state,lease_until,expires_at);
|
|
21
|
+
CREATE INDEX enterprise_queries_jobs_domain ON enterprise_queries.procrastinate_jobs((args->>'query_id'),task_name,status);
|
|
22
|
+
CREATE TABLE enterprise_queries.events(
|
|
23
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
24
|
+
query_id uuid NOT NULL,account_id text NOT NULL,principal_key text NOT NULL,
|
|
25
|
+
event text NOT NULL CHECK(event IN('submitted','claimed','ready','released','retry_pending','failed','cancelled','expired')),
|
|
26
|
+
attempt integer NOT NULL,created_at timestamptz NOT NULL DEFAULT clock_timestamp()
|
|
27
|
+
);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Native credential/engine composition for queued questions and delayed release."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import threading
|
|
4
|
+
import logging
|
|
5
|
+
import procrastinate
|
|
6
|
+
import psycopg
|
|
7
|
+
from enterprise_queries import QueryLeaseLost
|
|
8
|
+
from enterprise_sources import SourceDenied,SourceUnavailable,SourceChanged
|
|
9
|
+
from public_faq import InvalidAnswer
|
|
10
|
+
from enterprise_knowledge import current_engine
|
|
11
|
+
from enterprise_audit import KnowledgeAudit
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EnterpriseAnswers:
|
|
15
|
+
def __init__(self,store,connections,make_engine,*,extraction_cache=None,audit=None):
|
|
16
|
+
if not isinstance(connections,dict)or not 1<=len(connections)<=20 or not callable(make_engine):raise ValueError('Supply owned native question adapters')
|
|
17
|
+
if any(entry.driver.store is not store.auth for entry in connections.values()):raise ValueError('Question/source stores must use the same owned authority')
|
|
18
|
+
if extraction_cache is not None and(extraction_cache.auth is not store.auth or extraction_cache.account_id!=store.account_id):raise ValueError('Extraction cache must use the same owned company and authority')
|
|
19
|
+
if audit is not None and(not isinstance(audit,KnowledgeAudit)or audit.auth is not store.auth or audit.account_id!=store.account_id):raise ValueError('Question audit must use the same owned company and authority')
|
|
20
|
+
self.store=store;self.connections=connections;self.make_engine=make_engine;self.extraction_cache=extraction_cache;self.audit=audit
|
|
21
|
+
|
|
22
|
+
def engine(self,principal,authorize,*,query_id=None,attempt=None):
|
|
23
|
+
engine=current_engine(self.store.auth,self.connections,principal,self.make_engine,authorize)
|
|
24
|
+
if self.audit is not None:engine.audit=self.audit.bind(principal,query_id=query_id,attempt=attempt)
|
|
25
|
+
if self.extraction_cache is not None:
|
|
26
|
+
engine.decode=self.extraction_cache.wrap(engine.decode,lambda:engine.authorize(principal.key))
|
|
27
|
+
return engine
|
|
28
|
+
|
|
29
|
+
def execute(self,identity):
|
|
30
|
+
lease=self.store.claim(identity)
|
|
31
|
+
if lease is None:return
|
|
32
|
+
stop=threading.Event();lost=threading.Event()
|
|
33
|
+
def heartbeat():
|
|
34
|
+
while not stop.wait(max(1,self.store.lease_seconds//3)):
|
|
35
|
+
try:self.store.guard(lease,renew=True)
|
|
36
|
+
except Exception:lost.set();return
|
|
37
|
+
def authorize(subject):
|
|
38
|
+
if lost.is_set():raise QueryLeaseLost('Question worker lost current authority')
|
|
39
|
+
if subject!=lease.principal.key:raise SourceDenied('company_identity_required')
|
|
40
|
+
self.store.guard(lease)
|
|
41
|
+
thread=threading.Thread(target=heartbeat,daemon=True);thread.start()
|
|
42
|
+
try:
|
|
43
|
+
authorize(lease.principal.key)
|
|
44
|
+
engine=self.engine(lease.principal,authorize,query_id=lease.id,attempt=lease.attempt)
|
|
45
|
+
receipt=engine.answer_receipt(lease.question,lease.principal.key)
|
|
46
|
+
authorize(lease.principal.key)
|
|
47
|
+
self.store.complete(lease,receipt)
|
|
48
|
+
except QueryLeaseLost:return
|
|
49
|
+
except SourceDenied as error:
|
|
50
|
+
code='setup'if str(error)=='source_mapping_required'else'source'if str(error)in {'source_reconnection_required','source_consent_required'}else'signin'if str(error).startswith('company_')else'unavailable'
|
|
51
|
+
self.store.failed(lease,code,retry=code=='unavailable')
|
|
52
|
+
except InvalidAnswer:self.store.failed(lease,'answer')
|
|
53
|
+
except SourceUnavailable:self.store.failed(lease,'unavailable',retry=True)
|
|
54
|
+
except Exception:self.store.failed(lease,'unavailable')
|
|
55
|
+
finally:stop.set();thread.join(timeout=2)
|
|
56
|
+
|
|
57
|
+
def status(self,session,identity):
|
|
58
|
+
row=self.store.get(session,identity)
|
|
59
|
+
result={'id':identity,'status':row['state']}
|
|
60
|
+
if row['state']=='failed':result['errorCode']=row['error_code'];return result
|
|
61
|
+
if row['state']!='ready':return result
|
|
62
|
+
receipt=self.store.open_result(row)
|
|
63
|
+
def authorize(subject):
|
|
64
|
+
if subject!=session.principal.key:raise SourceDenied('company_identity_required')
|
|
65
|
+
current=self.store.get(session,identity)
|
|
66
|
+
if current['state']!='ready'or current['attempt']!=row['attempt']:raise QueryLeaseLost('Question changed before release')
|
|
67
|
+
try:
|
|
68
|
+
engine=self.engine(session.principal,authorize,query_id=identity,attempt=row['attempt'])
|
|
69
|
+
answer=engine.release(receipt,session.principal.key)
|
|
70
|
+
self.store.released(session,identity,row['attempt'])
|
|
71
|
+
return {**result,'result':answer}
|
|
72
|
+
except SourceChanged:
|
|
73
|
+
self.store.requeue(session,identity,row['attempt'])
|
|
74
|
+
return {'id':identity,'status':'queued'}
|
|
75
|
+
except QueryLeaseLost:
|
|
76
|
+
return {'id':identity,'status':self.store.get(session,identity)['state']}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class EnterpriseQueryWorker:
|
|
80
|
+
def __init__(self,dsn,service,*,document_recovery=None):
|
|
81
|
+
if document_recovery is not None and not callable(document_recovery):raise ValueError('Use the owned document reconciler')
|
|
82
|
+
self.service=service
|
|
83
|
+
self.document_recovery=document_recovery
|
|
84
|
+
async def refresh():return await asyncio.to_thread(dsn)
|
|
85
|
+
self.app=procrastinate.App(connector=procrastinate.PsycopgConnector(conninfo=refresh if callable(dsn)else dsn,min_size=1,max_size=4,max_lifetime=600,kwargs={'connect_timeout':5,'options':'-c search_path=pg_catalog,enterprise_queries,pg_temp -c statement_timeout=10000 -c lock_timeout=3000'}))
|
|
86
|
+
async def execute(query_id):await asyncio.to_thread(service.execute,query_id)
|
|
87
|
+
self.task=self.app.task(name=service.store.task.name,queue=service.store.task.queue)(execute)
|
|
88
|
+
|
|
89
|
+
async def run(self,*,concurrency=2,**options):
|
|
90
|
+
if type(concurrency)is not int or not 1<=concurrency<=4:raise ValueError('Use bounded owned query concurrency')
|
|
91
|
+
async def recover():
|
|
92
|
+
while True:
|
|
93
|
+
try:
|
|
94
|
+
await asyncio.to_thread(self.service.store.recover)
|
|
95
|
+
if self.service.extraction_cache is not None:await asyncio.to_thread(self.service.extraction_cache.purge)
|
|
96
|
+
except(psycopg.OperationalError,SourceUnavailable):logging.getLogger(__name__).warning('Question recovery is temporarily unavailable')
|
|
97
|
+
await asyncio.sleep(15)
|
|
98
|
+
async def documents():
|
|
99
|
+
# Separate loop: slow native deletion never blocks query lease or
|
|
100
|
+
# cache recovery. SQL cleanup claims prevent overlapping ownership.
|
|
101
|
+
while True:
|
|
102
|
+
try:await asyncio.to_thread(self.document_recovery,limit=1)
|
|
103
|
+
except(psycopg.Error,SourceUnavailable):logging.getLogger(__name__).warning('Document cleanup is temporarily unavailable')
|
|
104
|
+
await asyncio.sleep(15)
|
|
105
|
+
async with self.app.open_async():
|
|
106
|
+
async with asyncio.TaskGroup()as group:
|
|
107
|
+
recovery=group.create_task(recover())
|
|
108
|
+
cleanup=group.create_task(documents())if self.document_recovery is not None else None
|
|
109
|
+
await self.app.run_worker_async(queues=[self.task.queue],concurrency=concurrency,**options)
|
|
110
|
+
recovery.cancel()
|
|
111
|
+
if cleanup is not None:cleanup.cancel()
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Startup secret injections for the owned read-only private assistant.
|
|
2
|
+
|
|
3
|
+
Native platforms resolve pinned secret references. This adapter never downloads
|
|
4
|
+
cloud secrets, generates a replacement encryption key or writes into an export.
|
|
5
|
+
All replicas must use the same reviewed encryption-key version; coordinated key
|
|
6
|
+
migration/backup is separate operator work, not automatic hot rotation.
|
|
7
|
+
"""
|
|
8
|
+
import atexit
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import shutil
|
|
12
|
+
import tempfile
|
|
13
|
+
from cryptography.fernet import Fernet
|
|
14
|
+
from enterprise_sources import SourceUnavailable
|
|
15
|
+
|
|
16
|
+
FIELDS = {
|
|
17
|
+
'encryption': ('ENTERPRISE_ENCRYPTION_KEY', 'ENTERPRISE_ENCRYPTION_KEY_FILE', 44),
|
|
18
|
+
'graph': ('SHAREPOINT_CLIENT_SECRET', 'SHAREPOINT_CLIENT_SECRET_FILE', 16000),
|
|
19
|
+
'confluence': ('CONFLUENCE_CLIENT_SECRET', 'CONFLUENCE_CLIENT_SECRET_FILE', 16000),
|
|
20
|
+
'database_password': ('DATABASE_PASSWORD', 'DATABASE_PASSWORD_FILE', 500),
|
|
21
|
+
'database_ca': ('DATABASE_CA_PEM', 'DATABASE_CA_FILE', 256000),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def decode(name, raw):
|
|
26
|
+
if not raw or len(raw) > FIELDS[name][2] or b'\0' in raw:
|
|
27
|
+
raise ValueError('Invalid owned secret value')
|
|
28
|
+
value = raw.decode('utf-8')
|
|
29
|
+
if name == 'encryption':
|
|
30
|
+
if len(raw) != 44:
|
|
31
|
+
raise ValueError('Use the existing exact Fernet key')
|
|
32
|
+
Fernet(raw)
|
|
33
|
+
if name == 'database_ca' and '-----BEGIN CERTIFICATE-----' not in value:
|
|
34
|
+
raise ValueError('Use the native database CA certificate')
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SecretFiles:
|
|
39
|
+
def __init__(self, provider, environment, role):
|
|
40
|
+
if provider not in {'aws', 'azure', 'gcp', 'ibm'} or role not in {'api', 'worker', 'operator'}:
|
|
41
|
+
raise ValueError('Supply the owned private workload provider and role')
|
|
42
|
+
# Read-only assistants have no action credentials. Native workload
|
|
43
|
+
# identities must not silently fall back to inherited developer keys.
|
|
44
|
+
forbidden = ['DESTINATION_TOKEN', 'DESTINATION_TOKEN_FILE', 'PARSER_SHARED_TOKEN', 'PARSER_SHARED_TOKEN_FILE',
|
|
45
|
+
'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', 'AZURE_CLIENT_SECRET',
|
|
46
|
+
'GOOGLE_APPLICATION_CREDENTIALS', 'IBMCLOUD_API_KEY', 'OPENAI_API_KEY']
|
|
47
|
+
if provider != 'ibm': forbidden += ['DATABASE_PASSWORD', 'DATABASE_PASSWORD_FILE']
|
|
48
|
+
if role == 'operator': forbidden += [value for name in ['encryption', 'graph', 'confluence'] for value in FIELDS[name][:2]]
|
|
49
|
+
if any(key in environment for key in forbidden):
|
|
50
|
+
raise ValueError('Unexpected static cloud, parser or action authority in the private workload')
|
|
51
|
+
self.env, self.directory, self.closed = dict(environment), None, False
|
|
52
|
+
required = ['encryption', 'graph', 'confluence'] if role != 'operator' else []
|
|
53
|
+
if provider != 'gcp': required.append('database_ca')
|
|
54
|
+
if provider == 'ibm': required.append('database_password')
|
|
55
|
+
selected = []
|
|
56
|
+
for name, (direct, pointer, maximum) in FIELDS.items():
|
|
57
|
+
if direct in environment and pointer in environment:
|
|
58
|
+
raise ValueError('Conflicting native secret injection and mounted file')
|
|
59
|
+
if direct in environment:
|
|
60
|
+
value = environment[direct]
|
|
61
|
+
if not isinstance(value, str): raise ValueError('Invalid native secret injection')
|
|
62
|
+
decode(name, value.encode('utf-8'))
|
|
63
|
+
selected.append((name, direct, pointer, value))
|
|
64
|
+
elif pointer in environment:
|
|
65
|
+
path = environment[pointer]
|
|
66
|
+
if not isinstance(path, str) or not Path(path).is_absolute():
|
|
67
|
+
raise ValueError('Use the owned mounted secret path')
|
|
68
|
+
elif name in required:
|
|
69
|
+
raise ValueError('Missing required native secret reference')
|
|
70
|
+
try:
|
|
71
|
+
if selected:
|
|
72
|
+
self.directory = Path(tempfile.mkdtemp(prefix='atmem-enterprise-secrets-'))
|
|
73
|
+
self.directory.chmod(0o700)
|
|
74
|
+
for name, direct, pointer, value in selected:
|
|
75
|
+
path = self.directory / name
|
|
76
|
+
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
77
|
+
with os.fdopen(descriptor, 'wb') as stream: stream.write(value.encode('utf-8'))
|
|
78
|
+
self.env[pointer] = str(path); self.env.pop(direct, None)
|
|
79
|
+
# Fail startup for unreadable/invalid required files; do not generate
|
|
80
|
+
# new keys, invent credentials or defer broken setup to a basic user.
|
|
81
|
+
for name in required: self.read(name)
|
|
82
|
+
if environment is os.environ:
|
|
83
|
+
for _, direct, _, _ in selected: os.environ.pop(direct, None)
|
|
84
|
+
atexit.register(self.close)
|
|
85
|
+
except BaseException:
|
|
86
|
+
self.close(); raise
|
|
87
|
+
|
|
88
|
+
def __repr__(self): return '<Owned private-assistant secret files>'
|
|
89
|
+
|
|
90
|
+
def read(self, name):
|
|
91
|
+
if name not in FIELDS: raise ValueError('Unknown owned secret purpose')
|
|
92
|
+
try:
|
|
93
|
+
if self.closed: raise ValueError()
|
|
94
|
+
_, pointer, maximum = FIELDS[name]
|
|
95
|
+
path = self.env[pointer]
|
|
96
|
+
with Path(path).open('rb') as stream: raw = stream.read(maximum + 1)
|
|
97
|
+
return decode(name, raw)
|
|
98
|
+
except Exception:
|
|
99
|
+
raise SourceUnavailable('enterprise_secret_unavailable') from None
|
|
100
|
+
|
|
101
|
+
def reader(self, name):
|
|
102
|
+
if name not in FIELDS: raise ValueError('Unknown owned secret purpose')
|
|
103
|
+
return lambda: self.read(name)
|
|
104
|
+
|
|
105
|
+
def close(self):
|
|
106
|
+
if self.directory is not None: shutil.rmtree(self.directory, ignore_errors=True)
|
|
107
|
+
self.closed = True
|
|
108
|
+
|
|
109
|
+
def __enter__(self): return self
|
|
110
|
+
def __exit__(self, *args): self.close()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Opaque, durable company sessions; raw cookies/JWTs never persist in SQL."""
|
|
2
|
+
from dataclasses import dataclass,field
|
|
3
|
+
import hmac
|
|
4
|
+
import secrets
|
|
5
|
+
import time
|
|
6
|
+
import re
|
|
7
|
+
from enterprise_auth_store import digest,secret_binding
|
|
8
|
+
from enterprise_identity import CompanyPrincipal
|
|
9
|
+
from enterprise_sources import SourceDenied
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class BrowserSession:
|
|
14
|
+
principal: CompanyPrincipal
|
|
15
|
+
id: str = field(repr=False)
|
|
16
|
+
browser_nonce: str = field(repr=False)
|
|
17
|
+
csrf_sha256: str = field(repr=False)
|
|
18
|
+
csrf: str = field(repr=False)
|
|
19
|
+
|
|
20
|
+
def require_csrf(self,token):
|
|
21
|
+
secret_binding(token)
|
|
22
|
+
if not hmac.compare_digest(digest(token),self.csrf_sha256):raise SourceDenied('browser_request_denied')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class CreatedSession:
|
|
27
|
+
token: str = field(repr=False)
|
|
28
|
+
csrf: str = field(repr=False)
|
|
29
|
+
max_age: int
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CompanySessions:
|
|
33
|
+
def __init__(self,auth,*,domain=None):
|
|
34
|
+
if domain is not None and(not isinstance(domain,str)or not re.fullmatch('[0-9a-f]{64}',domain)):raise ValueError('Use the owned company session domain')
|
|
35
|
+
if getattr(auth,'company_domain',None)!=domain:raise ValueError('Session and authority company registrations must match')
|
|
36
|
+
self.auth=auth;self.domain=domain
|
|
37
|
+
def context(self,account,key,session):
|
|
38
|
+
result=['browser-session',account,key,session]
|
|
39
|
+
if self.domain is not None:result+=['company-registration',self.domain]
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
def private(self,row,identity):
|
|
43
|
+
try:
|
|
44
|
+
private=self.auth.box.open(self.context(row['account_id'],row['principal_key'],identity),row['nonce_ciphertext'])
|
|
45
|
+
if set(private)!={'browser_nonce','csrf'}:raise ValueError()
|
|
46
|
+
secret_binding(private['browser_nonce']);secret_binding(private['csrf'])
|
|
47
|
+
if not hmac.compare_digest(digest(private['csrf']),row['csrf_sha256']):raise ValueError()
|
|
48
|
+
return private
|
|
49
|
+
except Exception:raise SourceDenied('company_identity_required')from None
|
|
50
|
+
|
|
51
|
+
def create(self,principal,previous=None):
|
|
52
|
+
if previous:
|
|
53
|
+
try:secret_binding(previous)
|
|
54
|
+
except SourceDenied:previous=None
|
|
55
|
+
token=secrets.token_urlsafe(32);csrf=secrets.token_urlsafe(32);nonce=secrets.token_urlsafe(32);identity=digest(token)
|
|
56
|
+
with self.auth.transaction()as cur:
|
|
57
|
+
self.auth._authorize(cur,principal)
|
|
58
|
+
previous_owner=cur.execute('SELECT account_id,principal_key FROM enterprise_auth.sessions WHERE session_sha256=%s',(digest(previous),)).fetchone()if previous else None
|
|
59
|
+
owners={principal.key}
|
|
60
|
+
if previous_owner and previous_owner['account_id']==principal.account_id:owners.add(previous_owner['principal_key'])
|
|
61
|
+
# A profile switch touches the prior person's browser row and the new
|
|
62
|
+
# person's quota. Lock both owner domains in one deterministic order.
|
|
63
|
+
for owner in sorted(owners):cur.execute('SELECT pg_advisory_xact_lock(hashtextextended(%s,0))',('enterprise-session:'+principal.account_id+':'+owner,))
|
|
64
|
+
if previous_owner and previous_owner['account_id']==principal.account_id:
|
|
65
|
+
row=cur.execute('SELECT account_id,principal_key,nonce_ciphertext,csrf_sha256,revoked_at,expires_at>clock_timestamp()AS current FROM enterprise_auth.sessions WHERE session_sha256=%s FOR UPDATE',(digest(previous),)).fetchone()
|
|
66
|
+
if row and row['account_id']==principal.account_id:
|
|
67
|
+
if row['principal_key']==principal.key and row['current']and row['revoked_at']is None:
|
|
68
|
+
try:nonce=self.private(row,digest(previous))['browser_nonce']
|
|
69
|
+
except SourceDenied:pass # New verified login gets a fresh nonce; never inherit an invalid/old-domain one.
|
|
70
|
+
cur.execute('UPDATE enterprise_auth.sessions SET revoked_at=clock_timestamp()WHERE session_sha256=%s',(digest(previous),))
|
|
71
|
+
cur.execute("DELETE FROM enterprise_auth.sessions WHERE account_id=%s AND principal_key=%s AND expires_at<clock_timestamp()-interval '1 day'",(principal.account_id,principal.key))
|
|
72
|
+
cur.execute('UPDATE enterprise_auth.sessions SET revoked_at=clock_timestamp()WHERE session_sha256 IN(SELECT session_sha256 FROM enterprise_auth.sessions WHERE account_id=%s AND principal_key=%s AND revoked_at IS NULL ORDER BY created_at DESC OFFSET 9)',(principal.account_id,principal.key))
|
|
73
|
+
encrypted=self.auth.box.seal(self.context(principal.account_id,principal.key,identity),{'browser_nonce':nonce,'csrf':csrf})
|
|
74
|
+
cur.execute('INSERT INTO enterprise_auth.sessions(session_sha256,account_id,principal_key,csrf_sha256,nonce_ciphertext,expires_at)VALUES(%s,%s,%s,%s,%s,to_timestamp(%s))',(identity,principal.account_id,principal.key,digest(csrf),encrypted,principal.expires_at))
|
|
75
|
+
self.auth.event(cur,principal,'company-session','session_started')
|
|
76
|
+
return CreatedSession(token,csrf,max(1,min(3600,int(principal.expires_at-time.time()))))
|
|
77
|
+
|
|
78
|
+
def authenticate(self,token):
|
|
79
|
+
try:secret_binding(token)
|
|
80
|
+
except SourceDenied:raise SourceDenied('company_identity_required')from None
|
|
81
|
+
identity=digest(token)
|
|
82
|
+
with self.auth.transaction()as cur:
|
|
83
|
+
row=cur.execute('SELECT s.account_id,s.principal_key,s.csrf_sha256,s.nonce_ciphertext,extract(epoch FROM s.expires_at)::bigint AS expires_at,m.issuer,m.subject FROM enterprise_auth.sessions s JOIN enterprise_auth.memberships m USING(account_id,principal_key)WHERE s.session_sha256=%s AND s.revoked_at IS NULL AND s.expires_at>clock_timestamp()',(identity,)).fetchone()
|
|
84
|
+
if not row:raise SourceDenied('company_identity_required')
|
|
85
|
+
principal=CompanyPrincipal(row['account_id'],row['issuer'],row['subject'],row['expires_at'])
|
|
86
|
+
if principal.key!=row['principal_key']:raise SourceDenied('company_identity_required')
|
|
87
|
+
self.auth._authorize(cur,principal)
|
|
88
|
+
private=self.private(row,identity);nonce=private['browser_nonce'];csrf=private['csrf']
|
|
89
|
+
return BrowserSession(principal,identity,nonce,row['csrf_sha256'],csrf)
|
|
90
|
+
|
|
91
|
+
def revoke(self,session):
|
|
92
|
+
with self.auth.transaction()as cur:
|
|
93
|
+
self.auth._authorize(cur,session.principal)
|
|
94
|
+
cur.execute('UPDATE enterprise_auth.sessions SET revoked_at=clock_timestamp()WHERE session_sha256=%s AND account_id=%s AND principal_key=%s',(session.id,session.principal.account_id,session.principal.key))
|
|
95
|
+
self.auth.event(cur,session.principal,'company-session','session_ended')
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Native read-only SharePoint/Confluence source boundary.
|
|
2
|
+
|
|
3
|
+
Per-user delegated credentials come from a trusted OAuth broker, never chat/model
|
|
4
|
+
input or a shared crawler identity. No private documents or permissions are cached.
|
|
5
|
+
These API adapters do not implement that broker, an index, or the model API.
|
|
6
|
+
"""
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from html.parser import HTMLParser
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
from urllib.error import HTTPError, URLError
|
|
13
|
+
from urllib.parse import quote, urlencode, urlsplit, unquote
|
|
14
|
+
from urllib.request import Request, build_opener, HTTPRedirectHandler, ProxyHandler
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SourceUnavailable(RuntimeError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SourceDenied(SourceUnavailable):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SourceChanged(SourceUnavailable):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class DelegatedAccess:
|
|
31
|
+
subject: str
|
|
32
|
+
source_subject: str
|
|
33
|
+
token: str = field(repr=False)
|
|
34
|
+
expires_at: float
|
|
35
|
+
kind: str = 'delegated'
|
|
36
|
+
|
|
37
|
+
def require(self, subject, now=None):
|
|
38
|
+
now=time.time()if now is None else now
|
|
39
|
+
if self.kind!='delegated'or not subject or self.subject!=subject or not self.source_subject or not isinstance(self.token,str)or not self.token or len(self.token)>32768 or any(c in self.token for c in '\r\n')or not isinstance(self.expires_at,(int,float))or isinstance(self.expires_at,bool)or not now+5<self.expires_at<now+86401:
|
|
40
|
+
raise SourceDenied('source_identity_unavailable')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class SourceDocument:
|
|
45
|
+
source: str
|
|
46
|
+
id: str
|
|
47
|
+
version: str
|
|
48
|
+
title: str = field(repr=False)
|
|
49
|
+
text: str = field(repr=False)
|
|
50
|
+
url: str = field(repr=False)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Reply:
|
|
55
|
+
status: int
|
|
56
|
+
headers: dict
|
|
57
|
+
body: bytes = field(repr=False)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def checked_url(url,hosts):
|
|
61
|
+
try:parts=urlsplit(url);port=parts.port
|
|
62
|
+
except (TypeError,ValueError):raise SourceUnavailable('invalid_source_url')from None
|
|
63
|
+
if parts.scheme!='https'or parts.hostname not in hosts or port not in {None,443}or parts.username or parts.password or parts.fragment or len(url)>16000 or any(c in url for c in '\r\n'):
|
|
64
|
+
raise SourceUnavailable('invalid_source_url')
|
|
65
|
+
return url
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class NoRedirect(HTTPRedirectHandler):
|
|
69
|
+
def redirect_request(self,request,fp,code,msg,headers,newurl):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class NativeHttp:
|
|
74
|
+
def __init__(self):
|
|
75
|
+
# A source bearer token must not be routed through environment proxies.
|
|
76
|
+
self.opener=build_opener(ProxyHandler({}),NoRedirect())
|
|
77
|
+
|
|
78
|
+
def get(self,url,token,*,hosts,limit=2000000):
|
|
79
|
+
checked_url(url,hosts)
|
|
80
|
+
headers={'Accept':'application/json','Cache-Control':'no-store'}
|
|
81
|
+
if token:headers['Authorization']='Bearer '+token
|
|
82
|
+
try:
|
|
83
|
+
response=self.opener.open(Request(url,headers=headers),timeout=10)
|
|
84
|
+
except HTTPError as error:
|
|
85
|
+
try:
|
|
86
|
+
if error.code in {301,302,303,307,308}:
|
|
87
|
+
return Reply(error.code,dict(error.headers),b'')
|
|
88
|
+
if error.code in {401,403,404,410}:raise SourceDenied('source_access_denied')from None
|
|
89
|
+
raise SourceUnavailable('source_request_failed')from None
|
|
90
|
+
finally:error.close()
|
|
91
|
+
except (URLError,OSError,ValueError):raise SourceUnavailable('source_request_failed')from None
|
|
92
|
+
with response:
|
|
93
|
+
body=response.read(limit+1)
|
|
94
|
+
if len(body)>limit:raise SourceUnavailable('source_response_too_large')
|
|
95
|
+
return Reply(response.status,dict(response.headers),body)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def decoded(reply):
|
|
99
|
+
if reply.status!=200:raise SourceUnavailable('source_response_failed')
|
|
100
|
+
try:value=json.loads(reply.body)
|
|
101
|
+
except (ValueError,UnicodeError):raise SourceUnavailable('invalid_source_response')from None
|
|
102
|
+
if not isinstance(value,dict):raise SourceUnavailable('invalid_source_response')
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def identifier(value):
|
|
107
|
+
if not isinstance(value,str)or not re.fullmatch(r'[A-Za-z0-9_!.,-]{1,256}',value):raise SourceUnavailable('invalid_source_identifier')
|
|
108
|
+
return quote(value,safe='')
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def query_text(value):
|
|
112
|
+
if not isinstance(value,str)or not value.strip()or len(value)>1000 or any(ord(c)<32 for c in value):raise SourceUnavailable('invalid_source_query')
|
|
113
|
+
return value.strip()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class GraphSharePoint:
|
|
117
|
+
def __init__(self,drive_id,sharepoint_host,access,*,http=None):
|
|
118
|
+
self.drive_id=drive_id;self.drive=identifier(drive_id);self.source='sharepoint:'+drive_id
|
|
119
|
+
if not isinstance(sharepoint_host,str)or not re.fullmatch(r'[a-z0-9][a-z0-9-]*\.sharepoint\.com',sharepoint_host):raise SourceUnavailable('invalid_sharepoint_host')
|
|
120
|
+
self.host=sharepoint_host;self.access=access;self.http=http or NativeHttp()
|
|
121
|
+
self.base='https://graph.microsoft.com/v1.0'
|
|
122
|
+
|
|
123
|
+
def _get(self,path,subject):
|
|
124
|
+
self.access.require(subject)
|
|
125
|
+
return decoded(self.http.get(self.base+path,self.access.token,hosts={'graph.microsoft.com'}))
|
|
126
|
+
|
|
127
|
+
def identify(self,subject):
|
|
128
|
+
if self._get('/me?$select=id',subject).get('id')!=self.access.source_subject:raise SourceDenied('source_identity_mismatch')
|
|
129
|
+
|
|
130
|
+
def search(self,question,subject,*,limit=20):
|
|
131
|
+
if type(limit)is not int or not 1<=limit<=100:raise SourceUnavailable('invalid_search_limit')
|
|
132
|
+
self.identify(subject)
|
|
133
|
+
q=quote(query_text(question).replace("'","''"),safe='')
|
|
134
|
+
route=f"/drives/{self.drive}/root/search(q='{q}')"
|
|
135
|
+
url=self.base+route+'?'+urlencode({'$top':min(limit,100),'$select':'id,file,parentReference'})
|
|
136
|
+
result=[];seen=set();pages=set()
|
|
137
|
+
while url and len(result)<limit:
|
|
138
|
+
checked_url(url,{'graph.microsoft.com'})
|
|
139
|
+
if unquote(urlsplit(url).path)!=unquote(urlsplit(self.base+route).path)or url in pages or len(pages)>=20:raise SourceUnavailable('invalid_source_paging')
|
|
140
|
+
pages.add(url);self.access.require(subject)
|
|
141
|
+
value=decoded(self.http.get(url,self.access.token,hosts={'graph.microsoft.com'}))
|
|
142
|
+
if not isinstance(value.get('value'),list):raise SourceUnavailable('invalid_source_response')
|
|
143
|
+
for row in value['value']:
|
|
144
|
+
if not isinstance(row,dict):raise SourceUnavailable('invalid_source_response')
|
|
145
|
+
if row.get('parentReference',{}).get('driveId')!=self.drive_id or not isinstance(row.get('file'),dict):continue
|
|
146
|
+
identity=row.get('id');identifier(identity)
|
|
147
|
+
if identity not in seen:seen.add(identity);result.append(identity)
|
|
148
|
+
if len(result)==limit:break
|
|
149
|
+
url=value.get('@odata.nextLink')
|
|
150
|
+
return tuple(result)
|
|
151
|
+
|
|
152
|
+
def metadata(self,identity,subject):
|
|
153
|
+
value=self._get('/drives/'+self.drive+'/items/'+identifier(identity)+'?'+urlencode({'$select':'id,name,eTag,file,parentReference,webUrl,deleted'}),subject)
|
|
154
|
+
if value.get('id')!=identity or value.get('deleted')or not isinstance(value.get('file'),dict)or value.get('parentReference',{}).get('driveId')!=self.drive_id or not isinstance(value.get('eTag'),str)or not value['eTag']:
|
|
155
|
+
raise SourceDenied('source_document_unavailable')
|
|
156
|
+
checked_url(value.get('webUrl',''),{self.host})
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
def fetch(self,identity,subject,decode):
|
|
160
|
+
self.identify(subject);before=self.metadata(identity,subject)
|
|
161
|
+
self.access.require(subject)
|
|
162
|
+
reply=self.http.get(self.base+'/drives/'+self.drive+'/items/'+identifier(identity)+'/content',self.access.token,hosts={'graph.microsoft.com'},limit=10000000)
|
|
163
|
+
if reply.status==302:
|
|
164
|
+
# Download capability URL is short lived: never persist/log it and
|
|
165
|
+
# never forward the Graph bearer token to the storage endpoint.
|
|
166
|
+
location=reply.headers.get('Location')or reply.headers.get('location')
|
|
167
|
+
checked_url(location,{self.host})
|
|
168
|
+
reply=self.http.get(location,None,hosts={self.host},limit=10000000)
|
|
169
|
+
if reply.status!=200:raise SourceUnavailable('source_download_failed')
|
|
170
|
+
def current():
|
|
171
|
+
value=self.metadata(identity,subject)
|
|
172
|
+
if value['eTag']!=before['eTag']:raise SourceChanged('source_document_changed')
|
|
173
|
+
return value
|
|
174
|
+
source_decoder=getattr(decode,'from_source',None)
|
|
175
|
+
mime=before['file'].get('mimeType','application/octet-stream')
|
|
176
|
+
text=source_decoder(reply.body,mime,current)if callable(source_decoder)else decode(reply.body,mime)
|
|
177
|
+
if not isinstance(text,str)or not text.strip()or len(text)>200000:raise SourceUnavailable('source_document_unsupported')
|
|
178
|
+
after=current()
|
|
179
|
+
title=after.get('name')
|
|
180
|
+
if not isinstance(title,str)or not title or len(title)>1000:raise SourceUnavailable('invalid_source_response')
|
|
181
|
+
return SourceDocument(self.source,identity,after['eTag'],title,text,after['webUrl'])
|
|
182
|
+
|
|
183
|
+
def revalidate(self,document,subject):
|
|
184
|
+
if document.source!=self.source or self.metadata(document.id,subject)['eTag']!=document.version:raise SourceChanged('source_document_changed')
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class PageText(HTMLParser):
|
|
188
|
+
def __init__(self):super().__init__(convert_charrefs=True);self.parts=[];self.hidden=0
|
|
189
|
+
def handle_starttag(self,tag,attrs):
|
|
190
|
+
if tag in {'script','style'}:self.hidden+=1
|
|
191
|
+
elif not self.hidden and tag in {'p','br','div','li','h1','h2','h3','tr'}:self.parts.append('\n')
|
|
192
|
+
def handle_endtag(self,tag):
|
|
193
|
+
if tag in {'script','style'}and self.hidden:self.hidden-=1
|
|
194
|
+
def handle_data(self,data):
|
|
195
|
+
if not self.hidden:self.parts.append(data)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ConfluenceCloud:
|
|
199
|
+
def __init__(self,cloud_id,site_host,spaces,access,*,http=None):
|
|
200
|
+
if not isinstance(cloud_id,str)or not re.fullmatch(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}',cloud_id):raise SourceUnavailable('invalid_confluence_cloud')
|
|
201
|
+
if not isinstance(site_host,str)or not re.fullmatch(r'[a-z0-9][a-z0-9-]*\.atlassian\.net',site_host):raise SourceUnavailable('invalid_confluence_host')
|
|
202
|
+
if not isinstance(spaces,dict)or not spaces or len(spaces)>100 or any(not isinstance(k,str)or not re.fullmatch(r'[A-Za-z0-9_~-]{1,128}',k)or not isinstance(v,str)or not re.fullmatch(r'[0-9]{1,30}',v)for k,v in spaces.items()):raise SourceUnavailable('invalid_confluence_spaces')
|
|
203
|
+
self.source='confluence:'+cloud_id;self.cloud_id=cloud_id;self.base='https://api.atlassian.com/ex/confluence/'+cloud_id;self.host=site_host;self.space_ids=set(spaces.values());self.space_keys=set(spaces);self.access=access;self.http=http or NativeHttp()
|
|
204
|
+
|
|
205
|
+
def _get(self,path,subject):
|
|
206
|
+
self.access.require(subject)
|
|
207
|
+
return decoded(self.http.get(self.base+path,self.access.token,hosts={'api.atlassian.com'}))
|
|
208
|
+
|
|
209
|
+
def identify(self,subject):
|
|
210
|
+
self.access.require(subject)
|
|
211
|
+
value=decoded(self.http.get('https://api.atlassian.com/me',self.access.token,hosts={'api.atlassian.com'}))
|
|
212
|
+
if value.get('account_id')!=self.access.source_subject or value.get('account_status')!='active':raise SourceDenied('source_identity_mismatch')
|
|
213
|
+
reply=self.http.get('https://api.atlassian.com/oauth/token/accessible-resources',self.access.token,hosts={'api.atlassian.com'})
|
|
214
|
+
try:resources=json.loads(reply.body)
|
|
215
|
+
except (ValueError,UnicodeError):raise SourceUnavailable('invalid_source_response')from None
|
|
216
|
+
if reply.status!=200 or not isinstance(resources,list):raise SourceUnavailable('invalid_source_response')
|
|
217
|
+
allowed=[r for r in resources if isinstance(r,dict)and r.get('id')==self.cloud_id and r.get('url')=='https://'+self.host]
|
|
218
|
+
if len(allowed)!=1 or not isinstance(allowed[0].get('scopes'),list)or 'read:page:confluence'not in allowed[0]['scopes']or not {'search:confluence','read:content-details:confluence'}.intersection(allowed[0]['scopes']):raise SourceDenied('source_site_unavailable')
|
|
219
|
+
|
|
220
|
+
def search(self,question,subject,*,limit=20):
|
|
221
|
+
if type(limit)is not int or not 1<=limit<=100:raise SourceUnavailable('invalid_search_limit')
|
|
222
|
+
self.identify(subject);term=query_text(question).replace('\\','\\\\').replace('"','\\"')
|
|
223
|
+
# IDs and query are data, never executable caller-supplied CQL.
|
|
224
|
+
cql='type=page AND space in ('+','.join('"'+key+'"'for key in sorted(self.space_keys))+') AND text ~ "'+term+'"'
|
|
225
|
+
route='/wiki/rest/api/search';url=self.base+route+'?'+urlencode({'cql':cql,'limit':min(limit,25)})
|
|
226
|
+
result=[];seen=set();pages=set()
|
|
227
|
+
while url and len(result)<limit:
|
|
228
|
+
checked_url(url,{'api.atlassian.com'})
|
|
229
|
+
if urlsplit(url).path!=urlsplit(self.base+route).path or url in pages or len(pages)>=20:raise SourceUnavailable('invalid_source_paging')
|
|
230
|
+
self.access.require(subject);pages.add(url)
|
|
231
|
+
value=decoded(self.http.get(url,self.access.token,hosts={'api.atlassian.com'}))
|
|
232
|
+
if not isinstance(value.get('results'),list):raise SourceUnavailable('invalid_source_response')
|
|
233
|
+
for row in value['results']:
|
|
234
|
+
content=row.get('content',{})if isinstance(row,dict)else{}
|
|
235
|
+
identity=content.get('id')
|
|
236
|
+
if content.get('type')!='page':continue
|
|
237
|
+
if not isinstance(identity,str)or not re.fullmatch(r'[0-9]{1,30}',identity):raise SourceUnavailable('invalid_source_response')
|
|
238
|
+
if identity not in seen:seen.add(identity);result.append(identity)
|
|
239
|
+
if len(result)==limit:break
|
|
240
|
+
next_url=value.get('_links',{}).get('next');url=None
|
|
241
|
+
if next_url:
|
|
242
|
+
if next_url.startswith('/rest/api/search?'):url=self.base+'/wiki'+next_url
|
|
243
|
+
elif next_url.startswith('/wiki/rest/api/search?'):url=self.base+next_url
|
|
244
|
+
else:url=next_url
|
|
245
|
+
return tuple(result)
|
|
246
|
+
|
|
247
|
+
def fetch(self,identity,subject,decode=None):
|
|
248
|
+
if not isinstance(identity,str)or not re.fullmatch(r'[0-9]{1,30}',identity):raise SourceUnavailable('invalid_source_identifier')
|
|
249
|
+
self.identify(subject)
|
|
250
|
+
value=self._get('/wiki/api/v2/pages/'+identity+'?body-format=storage',subject)
|
|
251
|
+
if value.get('id')!=identity or value.get('status')!='current'or value.get('spaceId')not in self.space_ids:raise SourceDenied('source_document_unavailable')
|
|
252
|
+
version=value.get('version',{}).get('number');title=value.get('title');html=value.get('body',{}).get('storage',{}).get('value')
|
|
253
|
+
if type(version)is not int or version<1 or not isinstance(title,str)or not title or len(title)>1000 or not isinstance(html,str)or len(html)>1000000:raise SourceUnavailable('invalid_source_response')
|
|
254
|
+
parser=PageText();parser.feed(html);text=''.join(parser.parts).strip()
|
|
255
|
+
if not text or len(text)>200000:raise SourceUnavailable('source_document_unsupported')
|
|
256
|
+
# Construct a fixed same-site citation, never an API-supplied redirect URL.
|
|
257
|
+
return SourceDocument(self.source,identity,str(version),title,text,'https://'+self.host+'/wiki/pages/viewpage.action?pageId='+identity)
|
|
258
|
+
|
|
259
|
+
def revalidate(self,document,subject):
|
|
260
|
+
if document.source!=self.source or self.fetch(document.id,subject).version!=document.version:raise SourceChanged('source_document_changed')
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Owned static browser assets and public OIDC registration configuration."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
from fastapi.responses import FileResponse
|
|
7
|
+
from enterprise_identity import trusted_https
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def company_configuration(identity,config):
|
|
11
|
+
if not isinstance(config,dict)or set(config)!={'clientId','authorizationEndpoint','tokenEndpoint','scopes'}:raise ValueError('Supply only public company OIDC registration fields')
|
|
12
|
+
if not isinstance(config['clientId'],str)or not 1<=len(config['clientId'])<=200:raise ValueError('Use the owned public company client ID')
|
|
13
|
+
authorization=trusted_https(config['authorizationEndpoint']);token=trusted_https(config['tokenEndpoint'])
|
|
14
|
+
scopes=config['scopes']
|
|
15
|
+
if not isinstance(scopes,list)or not 3<=len(scopes)<=8 or any(not isinstance(scope,str)or not 1<=len(scope)<=300 or any(c.isspace()for c in scope)for scope in scopes)or len(set(scopes))!=len(scopes)or not {'openid','profile'}.issubset(scopes)or 'offline_access'in scopes or not any(scope==identity.scope or scope.endswith('/'+identity.scope)for scope in scopes):raise ValueError('Use OIDC and the owned delegated API scope without browser refresh credentials')
|
|
16
|
+
return {'issuer':identity.issuer,'clientId':config['clientId'],'authorizationEndpoint':authorization,'tokenEndpoint':token,'scopes':list(scopes)}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def install_web(app,identity,connections,config,static_root=None):
|
|
20
|
+
company=company_configuration(identity,config)
|
|
21
|
+
token=company['tokenEndpoint']
|
|
22
|
+
root=Path(static_root)if static_root is not None else Path(__file__).parent
|
|
23
|
+
lock=json.loads((root/'enterprise-web-lock.json').read_text())
|
|
24
|
+
if hashlib.sha256((root/'enterprise-web-app.js').read_bytes()).hexdigest()!=lock['bundleSha256']:raise ValueError('Use the exact reviewed browser bundle')
|
|
25
|
+
# Native source constructors already validate these tenant citation hosts.
|
|
26
|
+
hosts=sorted({entry.driver.host for entry in connections.values()})
|
|
27
|
+
public={'company':company,'citationHosts':hosts,'authorizationOrigins':['https://login.microsoftonline.com','https://auth.atlassian.com']}
|
|
28
|
+
@app.get('/api/web-config')
|
|
29
|
+
def web_config():return public
|
|
30
|
+
@app.get('/')
|
|
31
|
+
@app.get('/company-callback')
|
|
32
|
+
def index():return FileResponse(root/'enterprise-web.html',media_type='text/html')
|
|
33
|
+
@app.get('/assets/workspace.js')
|
|
34
|
+
def javascript():return FileResponse(root/'enterprise-web-app.js',media_type='application/javascript')
|
|
35
|
+
@app.get('/assets/workspace.css')
|
|
36
|
+
def stylesheet():return FileResponse(root/'enterprise-web.css',media_type='text/css')
|
|
37
|
+
@app.get('/assets/notices.txt')
|
|
38
|
+
def notices():return FileResponse(root/'enterprise-web-notices.txt',media_type='text/plain')
|
|
39
|
+
return "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self' "+urlsplit(token).scheme+'://'+urlsplit(token).netloc+"; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
|