Task — engineering-spec@1

"DOC third-party import — DocuSign / Adobe Sign / HelloSign migration with LTV (long-term-validation) preservation"

draftTASK-DOC-010
module doc · class product · priority p1 · created 2026-05-17 · shipped null
depends on TASK-DOC-001 · blocks none

§1 — Description (BCP-14 normative)

The DOC service MUST ship third-party import at services/doc/src/import/ supporting DocuSign + Adobe Sign + HelloSign, LTV preservation, idempotency, 4 memory audit kinds.

  1. MUST validate import_source against closed enum per DEC-1761.
  1. MUST dispatch per provider:
  1. MUST preserve LTV per DEC-1762 — verify existing signature is valid at ltv_verifier.rs::verify(pdf_bytes); do NOT add new signature. Store as-is in S3.
  1. MUST be idempotent per DEC-1764 via UNIQUE on (source_provider, source_doc_id, tenant_id).
  1. MUST store provider creds in KMS per DEC-1763 — tenant_third_party_creds.encrypted_credential_arn; CLO-only writes.
  1. MUST define tables at migration 0007: ```sql CREATE TABLE tenant_third_party_creds ( tenant_id UUID NOT NULL, provider TEXT NOT NULL CHECK (provider IN ('docusign','adobe_sign','hellosign')), encrypted_credential_arn TEXT NOT NULL, account_id TEXT, last_used_at TIMESTAMPTZ, set_by UUID NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (tenant_id, provider) ); ALTER TABLE tenant_third_party_creds ENABLE ROW LEVEL SECURITY; CREATE POLICY tp_creds_rls ON tenant_third_party_creds USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); GRANT UPDATE (encrypted_credential_arn, account_id, last_used_at, set_by, updated_at) ON tenant_third_party_creds TO cyberos_app;

CREATE TABLE doc_imports ( import_id UUID PRIMARY KEY, tenant_id UUID NOT NULL, source_provider TEXT NOT NULL, source_doc_id TEXT NOT NULL, document_id UUID NOT NULL, ltv_valid BOOLEAN NOT NULL, imported_by UUID NOT NULL, trace_id CHAR(32), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (tenant_id, source_provider, source_doc_id) ); ALTER TABLE doc_imports ENABLE ROW LEVEL SECURITY; CREATE POLICY imports_rls ON doc_imports USING (tenant_id = current_setting('auth.tenant_id')::uuid) WITH CHECK (tenant_id = current_setting('auth.tenant_id')::uuid); REVOKE UPDATE, DELETE ON doc_imports FROM cyberos_app; ```

  1. MUST expose endpoints: ``text PUT /v1/doc/third-party-creds (CLO-only) POST /v1/doc/import/{provider}/start (CLO-only; TASK-MCP-007 task) GET /v1/doc/import/jobs/{id} (status) GET /v1/doc/imports (list completed) ``
  1. MUST run as async TASK-MCP-007 task — provider listing + fetching may take hours for large customers.
  1. MUST populate TASK-DOC-007 lifecycle metadata from provider metadata if available (parties, dates).
  1. MUST emit 4 memory audit kinds per DEC-1765. PII per TASK-MEMORY-111: source_doc_id hashed; provider name (public) ok.
  1. MUST thread trace_id from start → fetch → verify → store → audit.
  1. MUST NOT add new signature per DEC-1762.
  1. MUST NOT duplicate import per DEC-1764.

§2 — Why this design

Why 3 providers (DEC-1760)? Cover 95% of enterprise e-sign market; extensible for niche.

Why LTV preservation (DEC-1762)? Adding new signature invalidates original; courts may not accept re-signed.

Why CLO-gated creds (DEC-1763)? Provider API keys grant access to all customer contracts; high privilege.

Why async (DEC-1764)? Large customers have thousands of docs; sync would timeout.


§3 — API contract

PUT    /v1/doc/third-party-creds
POST   /v1/doc/import/{provider}/start    body: {filter?: {from_date, to_date}, dry_run?: bool}
GET    /v1/doc/import/jobs/{id}

Sample import job status:

{
  "job_id": "uuid",
  "provider": "docusign",
  "status": "running",
  "total_count": 1247,
  "imported_count": 312,
  "failed_count": 5,
  "ltv_invalid_count": 2,
  "started_at": "2026-05-17T10:00:00Z"
}

§4 — Acceptance criteria

  1. 3 providers + import_source enum cardinality 4 (incl manual). 2. LTV preserved (no new signature added). 3. LTV invalid → flagged but still imported (with sev-2 audit). 4. Idempotent via UNIQUE constraint. 5. Re-import returns existing document_id (200, not 409). 6. CLO-only creds (403 for others). 7. CLO-only import trigger. 8. Async via TASK-MCP-007. 9. Lifecycle metadata populated from provider. 10. 4 memory audit kinds emitted. 11. PII scrubbed (source_doc_id SHA256). 12. RLS denies cross-tenant. 13. Trace_id preserved. 14. Dry-run lists without import. 15. Filter by date range respected. 16. Provider API creds in KMS only. 17. Append-only imports table via REVOKE. 18. Provider rate-limit respected (backoff). 19. PDF MIME validated. 20. Large import (1000+) completes within 30min.

§5 — Verification

#[tokio::test]
async fn docusign_imports_with_ltv_preserved() {
    let ctx = TestContext::with_docusign_creds().await;
    ctx.mock_docusign_envelope("env-123", signed_pdf_bytes).await;
    let job = ctx.import_from("docusign").await;
    ctx.wait_completion(job).await;
    let imports = ctx.list_imports().await;
    assert_eq!(imports.len(), 1);
    let doc = ctx.fetch_doc(imports[0].document_id).await;
    let stored = ctx.fetch_s3_doc(&doc.s3_key).await;
    assert_eq!(stored, signed_pdf_bytes);  // byte-identical, LTV preserved
}

#[tokio::test]
async fn idempotent_re_import() {
    let ctx = TestContext::with_completed_import().await;
    ctx.run_import_again().await;
    let imports = ctx.list_imports().await;
    let unique_doc_ids: HashSet<_> = imports.iter().map(|i| i.document_id).collect();
    assert_eq!(unique_doc_ids.len(), imports.len());  // no duplicates
}

#[tokio::test]
async fn ltv_invalid_flagged() {
    let ctx = TestContext::with_invalid_signature_pdf().await;
    let job = ctx.import_from("docusign").await;
    ctx.wait_completion(job).await;
    let imp = ctx.fetch_import(ctx.expected_import).await;
    assert_eq!(imp.ltv_valid, false);
}

// 5.4..5.10

§7 — Dependencies

Upstream: TASK-DOC-001. Cross-module: TASK-DOC-007 (lifecycle metadata population), TASK-DOC-011 (LTV verifier shared logic), TASK-MCP-007 (async task), TASK-AUTH-101 (CLO role), TASK-AUTH-105 (KMS), TASK-MEMORY-111 (PII).

§10 — Failure modes

FailureDetectionOutcomeRecovery
Provider API downretry w/ backoffsev-2; job=failedretry job
Invalid creds401job=failed; CLO notifiedre-enter creds
Provider rate limit429backoff; resumeinherent
LTV invalidverifier flagimport + ltv_valid=falsemanual review
PDF malformedparse errskip; sev-2 audit per docmanual handle
Source doc deleted in provider404mark missing; sev-3inherent
Large PDF (>100MB)S3 multipartinherentinherent
Duplicate source_doc_idUNIQUEskip; return existinginherent
Cross-tenant cred leakRLSinherentinherent
Provider deprecates v1 APIper-provider client versionsupgrade requiredmaintenance

§11 — Implementation notes


End of TASK-DOC-010 spec.