diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d9a0339..6e0ab57 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -5,7 +5,7 @@ on: permissions: contents: read concurrency: - group: pages + group: pages-${{ github.ref }} cancel-in-progress: true jobs: build: diff --git a/examples/postman/MAVULA-API-v1.postman_collection.json b/examples/postman/MAVULA-API-v1.postman_collection.json index fbb37de..b947316 100644 --- a/examples/postman/MAVULA-API-v1.postman_collection.json +++ b/examples/postman/MAVULA-API-v1.postman_collection.json @@ -1,13 +1,16 @@ { "info": { + "_postman_id": "46219cb0-ef74-4fb4-8b79-6f4e38aa4c8b", "name": "MAVULA API v1", "description": "Public identity, ledger and workbench integration workflows.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "variable": [ - { "key": "identity_base_url", "value": "https://identity.mavula.dev" }, - { "key": "ledger_base_url", "value": "https://ledger.mavula.dev" }, - { "key": "workbench_base_url", "value": "https://workbench.mavula.dev" }, + { "key": "identity_base_url", "value": "https://identity.mavula.io" }, + { "key": "ledger_base_url", "value": "https://ledger.mavula.io" }, + { "key": "workbench_base_url", "value": "https://workbench.mavula.io" }, + { "key": "client_id", "value": "" }, + { "key": "client_assertion", "value": "" }, { "key": "access_token", "value": "" }, { "key": "account_id", "value": "" }, { "key": "request_id", "value": "" } @@ -17,6 +20,7 @@ "name": "Identity", "item": [ { + "id": "b5087a27-65f4-42ae-943c-559b0af9ce81", "name": "Effective identity", "request": { "method": "GET", @@ -30,6 +34,7 @@ "name": "Accounts", "item": [ { + "id": "4ce12ccf-e45b-47dc-bf4d-8c67dfbdc421", "name": "Create account", "request": { "method": "POST", @@ -44,6 +49,7 @@ } }, { + "id": "096c1163-bf05-4e72-8b31-627dfeb29c6f", "name": "Submit freeze request", "request": { "method": "POST", @@ -63,6 +69,7 @@ "name": "Workbench", "item": [ { + "id": "bb96c02f-4210-4f78-b8ed-f540c466f835", "name": "Read platform status", "request": { "method": "GET", diff --git a/openapi/identity-access.public.v1.yaml b/openapi/identity-access.public.v1.yaml index 7434d0f..5108fa4 100644 --- a/openapi/identity-access.public.v1.yaml +++ b/openapi/identity-access.public.v1.yaml @@ -9,7 +9,7 @@ info: name: GNU Affero General Public License v3.0 only identifier: AGPL-3.0-only servers: - - url: https://identity.mavula.dev + - url: https://identity.mavula.io description: Identity Access public endpoint tags: - name: Discovery @@ -33,14 +33,14 @@ paths: application/json: schema: { $ref: '#/components/schemas/OpenIdConfiguration' } example: - issuer: https://identity.mavula.dev - authorization_endpoint: https://identity.mavula.dev/auth - token_endpoint: https://identity.mavula.dev/token - jwks_uri: https://identity.mavula.dev/jwks - revocation_endpoint: https://identity.mavula.dev/token/revocation + issuer: https://identity.mavula.io + authorization_endpoint: https://identity.mavula.io/auth + token_endpoint: https://identity.mavula.io/token + jwks_uri: https://identity.mavula.io/jwks + revocation_endpoint: https://identity.mavula.io/token/revocation response_types_supported: [code] grant_types_supported: [authorization_code, refresh_token, client_credentials] - token_endpoint_auth_methods_supported: [client_secret_basic, client_secret_post] + token_endpoint_auth_methods_supported: [none, private_key_jwt] id_token_signing_alg_values_supported: [PS256] /jwks: get: @@ -72,7 +72,7 @@ paths: '302': description: Redirect to the operator interaction or registered callback URI headers: - Location: { schema: { type: string, format: uri } } + Location: { schema: { type: string } } '400': { $ref: '#/components/responses/OAuthError' } /token: post: @@ -93,7 +93,14 @@ paths: authorization_code: value: { grant_type: authorization_code, code: auth_code_value, redirect_uri: 'https://app.example/callback', code_verifier: pkce_verifier } client_credentials: - value: { grant_type: client_credentials, client_id: service_client, client_secret: redacted, scope: internal.worker } + value: + grant_type: client_credentials + client_id: service_client + client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer + client_assertion: signed_private_key_jwt + resource: urn:mavula:ledger-core + tenant_id: tenant_01 + scope: internal.worker responses: '200': description: OAuth token response @@ -154,6 +161,15 @@ components: openid: Authenticate an operator. profile: Read effective identity claims. finance.read: Read tenant-scoped financial resources. + finance.write: Submit tenant-scoped financial operations. + finance.approve: Approve controlled financial operations. + configuration.write: Manage tenant product configuration. + compliance.manage: Manage regulatory interoperability operations. + audit.read: Read authorized audit evidence. + identity.admin: Manage institutional identity configuration. + workbench.read: Read Workbench operational state. + workbench.jobs.write: Submit supported Workbench jobs. + observability.read: Read authorized operational status. clientCredentials: tokenUrl: /token scopes: @@ -199,6 +215,10 @@ components: redirect_uri: { type: string, format: uri } code_verifier: { type: string } client_id: { type: string } + client_assertion_type: { type: string, const: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' } + client_assertion: { type: string } + resource: { type: string } + tenant_id: { type: string } RefreshTokenRequest: type: object additionalProperties: false @@ -208,15 +228,22 @@ components: refresh_token: { type: string } client_id: { type: string } scope: { type: string } + client_assertion_type: { type: string, const: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' } + client_assertion: { type: string } + resource: { type: string } + tenant_id: { type: string } ClientCredentialsTokenRequest: type: object additionalProperties: false - required: [grant_type] + required: [grant_type, client_id, client_assertion_type, client_assertion, resource] properties: grant_type: { const: client_credentials } client_id: { type: string } - client_secret: { type: string, format: password } scope: { type: string } + client_assertion_type: { type: string, const: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' } + client_assertion: { type: string } + resource: { type: string } + tenant_id: { type: string } RevokeTokenRequest: type: object additionalProperties: false @@ -225,7 +252,8 @@ components: token: { type: string } token_type_hint: { type: string, enum: [access_token, refresh_token] } client_id: { type: string } - client_secret: { type: string, format: password } + client_assertion_type: { type: string, const: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer' } + client_assertion: { type: string } TokenResponse: type: object required: [access_token, token_type, expires_in] diff --git a/openapi/ledger-core.public.v1.yaml b/openapi/ledger-core.public.v1.yaml index 91d0e56..ffa68b8 100644 --- a/openapi/ledger-core.public.v1.yaml +++ b/openapi/ledger-core.public.v1.yaml @@ -9,7 +9,7 @@ info: name: GNU Affero General Public License v3.0 only identifier: AGPL-3.0-only servers: - - url: https://ledger.mavula.dev + - url: https://ledger.mavula.io description: Ledger Core public endpoint security: [{ bearerAuth: [] }] tags: @@ -717,8 +717,8 @@ components: required: [account_code] properties: account_code: { type: string, minLength: 1, maxLength: 64 } - debit_amount: { type: string } - credit_amount: { type: string } + debit_amount: { type: string, pattern: '^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,2})?$' } + credit_amount: { type: string, pattern: '^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,2})?$' } AccountPosting: type: object additionalProperties: false @@ -726,7 +726,7 @@ components: properties: account_id: { type: string } direction: { type: string, enum: [DEBIT, CREDIT] } - amount: { type: string } + amount: { type: string, pattern: '^(?:[1-9][0-9]*(?:\.[0-9]{1,2})?|0\.(?:0[1-9]|[1-9][0-9]?))$' } currency: { type: string, pattern: '^[A-Z]{3}$' } reference: { type: string, maxLength: 160 } FinancialCorrection: @@ -737,12 +737,15 @@ components: type: object required: [amount, currency] properties: - amount: { type: string } + amount: { type: string, pattern: '^(?:[1-9][0-9]*(?:\.[0-9]{1,2})?|0\.(?:0[1-9]|[1-9][0-9]?))$' } currency: { type: string, pattern: '^[A-Z]{3}$' } allocation: type: object required: [principal, interest, fees] - properties: { principal: { type: string }, interest: { type: string }, fees: { type: string } } + properties: + principal: { type: string, pattern: '^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,2})?$' } + interest: { type: string, pattern: '^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,2})?$' } + fees: { type: string, pattern: '^(?:0|[1-9][0-9]*)(?:\.[0-9]{1,2})?$' } journal: type: object required: [ledger_lines] @@ -760,28 +763,32 @@ components: reason: { type: string, minLength: 1, maxLength: 500 } correction: { $ref: '#/components/schemas/FinancialCorrection' } FinancialAdjustmentRequest: - allOf: - - $ref: '#/components/schemas/CreateFinancialAdjustment' - - type: object - required: [id, status, requested_by, correlation_id, created_at] - properties: - id: { type: string } - status: { $ref: '#/components/schemas/DecisionStatus' } - target_transaction_id: { type: string } - target_journal_entry_id: { type: string } - target_loan_id: { type: string } - requested_by: { type: string } - decided_by: { type: string } - decision_reason: { type: string } - failure_reason: { type: string } - correlation_id: { type: string } - reversal_transaction_id: { type: string } - reversal_journal_entry_id: { type: string } - replacement_transaction_id: { type: string } - replacement_journal_entry_id: { type: string } - created_at: { type: string, format: date-time } - decided_at: { type: string, format: date-time } - applied_at: { type: string, format: date-time } + type: object + additionalProperties: false + required: [id, target_type, target_id, adjustment_type, status, reason, requested_by, correlation_id, created_at] + properties: + id: { type: string } + target_type: { type: string, enum: [TRANSACTION, JOURNAL_ENTRY] } + target_id: { type: string, minLength: 1, maxLength: 128 } + adjustment_type: { type: string, enum: [REVERSAL, CORRECTION] } + status: { $ref: '#/components/schemas/DecisionStatus' } + reason: { type: string, minLength: 1, maxLength: 500 } + correction: { $ref: '#/components/schemas/FinancialCorrection' } + target_transaction_id: { type: string } + target_journal_entry_id: { type: string } + target_loan_id: { type: string } + requested_by: { type: string } + decided_by: { type: string } + decision_reason: { type: string } + failure_reason: { type: string } + correlation_id: { type: string } + reversal_transaction_id: { type: string } + reversal_journal_entry_id: { type: string } + replacement_transaction_id: { type: string } + replacement_journal_entry_id: { type: string } + created_at: { type: string, format: date-time } + decided_at: { type: string, format: date-time } + applied_at: { type: string, format: date-time } FinancialAdjustmentPage: type: object required: [items] diff --git a/openapi/workbench.public.v1.yaml b/openapi/workbench.public.v1.yaml index 7cd1255..2bfcb0e 100644 --- a/openapi/workbench.public.v1.yaml +++ b/openapi/workbench.public.v1.yaml @@ -9,7 +9,7 @@ info: name: GNU Affero General Public License v3.0 only identifier: AGPL-3.0-only servers: - - url: https://workbench.mavula.dev + - url: https://workbench.mavula.io description: Workbench public endpoint security: [{ bearerAuth: [] }] tags: @@ -27,6 +27,9 @@ paths: description: Submit tenant-scoped payment work. The tenant is always derived from the access token. tags: [Jobs] x-mavula-permissions: [workbench.jobs.write] + parameters: + - { $ref: '#/components/parameters/IdempotencyKey' } + - { $ref: '#/components/parameters/CorrelationId' } requestBody: required: true content: @@ -35,9 +38,7 @@ paths: examples: payment_capture: value: - queue: payments type: PAYMENT_CAPTURE - max_attempts: 3 payload: idempotency_key: payment_capture_20260715_001 correlation_id: checkout_20260715_001 @@ -52,6 +53,8 @@ paths: '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } + '409': { $ref: '#/components/responses/Conflict' } + '503': { $ref: '#/components/responses/ServiceUnavailable' } /api/jobs/{jobId}: get: operationId: getJob @@ -289,18 +292,40 @@ components: additionalProperties: false properties: { limit: { type: integer, minimum: 1, maximum: 500 } } CreateJob: + oneOf: + - $ref: '#/components/schemas/CreatePaymentCaptureJob' + - $ref: '#/components/schemas/CreatePaymentDisbursementJob' + - $ref: '#/components/schemas/CreatePaymentSettlementJob' + - $ref: '#/components/schemas/CreatePaymentReconciliationJob' + discriminator: { propertyName: type } + CreatePaymentCaptureJob: type: object additionalProperties: false required: [type, payload] properties: - type: { type: string, enum: [PAYMENT_CAPTURE, PAYMENT_DISBURSEMENT, PAYMENT_SETTLEMENT, PAYMENT_RECONCILIATION] } - queue: { type: string, const: payments, default: payments } - payload: - oneOf: - - $ref: '#/components/schemas/PaymentStartPayload' - - $ref: '#/components/schemas/PaymentSettlementPayload' - - $ref: '#/components/schemas/PaymentReconciliationPayload' - max_attempts: { type: integer, minimum: 1, default: 3 } + type: { type: string, const: PAYMENT_CAPTURE } + payload: { $ref: '#/components/schemas/PaymentStartPayload' } + CreatePaymentDisbursementJob: + type: object + additionalProperties: false + required: [type, payload] + properties: + type: { type: string, const: PAYMENT_DISBURSEMENT } + payload: { $ref: '#/components/schemas/PaymentStartPayload' } + CreatePaymentSettlementJob: + type: object + additionalProperties: false + required: [type, payload] + properties: + type: { type: string, const: PAYMENT_SETTLEMENT } + payload: { $ref: '#/components/schemas/PaymentSettlementPayload' } + CreatePaymentReconciliationJob: + type: object + additionalProperties: false + required: [type, payload] + properties: + type: { type: string, const: PAYMENT_RECONCILIATION } + payload: { $ref: '#/components/schemas/PaymentReconciliationPayload' } WorkerJob: type: object required: [id, queue, type, tenant_id, payload, status, attempts, max_attempts, created_at, updated_at] @@ -397,12 +422,19 @@ components: payload: { type: object, additionalProperties: true } PlatformStatus: type: object + additionalProperties: false + required: [status, service, version, environment, started_at, uptime_seconds, dependencies, worker, schedules, queues] properties: service: { type: string } status: { type: string, enum: [ok, degraded, down] } - timestamp: { type: string, format: date-time } + version: { type: string } + environment: { type: string } + started_at: { type: string, format: date-time } + uptime_seconds: { type: integer, minimum: 0 } worker: { type: object, additionalProperties: true } dependencies: { type: object, additionalProperties: true } + schedules: { type: array, items: { $ref: '#/components/schemas/ScheduledJob' } } + queues: { type: array, items: { $ref: '#/components/schemas/QueueStats' } } HttpError: type: object required: [statusCode, message] @@ -432,3 +464,6 @@ components: Conflict: description: Idempotency key, state, or delivery reference conflicts with existing state content: { application/json: { schema: { $ref: '#/components/schemas/HttpError' } } } + ServiceUnavailable: + description: Durable receipt storage or queue execution is temporarily unavailable + content: { application/json: { schema: { $ref: '#/components/schemas/HttpError' } } } diff --git a/sources.lock.json b/sources.lock.json index 6b860c4..ec41afe 100644 --- a/sources.lock.json +++ b/sources.lock.json @@ -5,19 +5,19 @@ "file": "identity-access.public.v1.yaml", "owner": "mavulahq/identity-access", "source": "contracts/openapi/identity-access.public.v1.yaml", - "sha256": "a126f53455b05bd8377fa12b81f814ae5d677acbbb0e8df33a706a5a0cf60a38" + "sha256": "2652296a97abed2726df27f23348101fe700cffee84fe4294aee524c957db30a" }, { "file": "ledger-core.public.v1.yaml", "owner": "mavulahq/ledger-core", "source": "contracts/openapi/ledger-core.public.v1.yaml", - "sha256": "115cda1bff1aaea6a7aaad81577a0918e4ced2ec222cefc4081433a08778f2a1" + "sha256": "dcae9e81479b62ac5641eb20a8a231628bfab9fc6edec3477b1e128a7acdc3e7" }, { "file": "workbench.public.v1.yaml", "owner": "mavulahq/workbench", "source": "contracts/openapi/workbench.public.v1.yaml", - "sha256": "1b6e8a3fdb3c882c87932e6966612ea71706902adabd5f6ddd3bf356762e95d9" + "sha256": "14053111b3b8b94a5f0fbd9ead1d38b0f6fc491e1accace4568a87e2e135abd8" } ] } diff --git a/src/content/docs/v1/getting-started/authentication.mdx b/src/content/docs/v1/getting-started/authentication.mdx index f1e4939..8ea2c79 100644 --- a/src/content/docs/v1/getting-started/authentication.mdx +++ b/src/content/docs/v1/getting-started/authentication.mdx @@ -20,7 +20,7 @@ Identity Access publishes OpenID Provider metadata at `/.well-known/openid-confi Generate a random `code_verifier`, derive its SHA-256 `code_challenge`, then redirect the operator to `/auth`. The `state` and `nonce` values must be unique and verified on return. ```text -GET https://identity.mavula.dev/auth +GET https://identity.mavula.io/auth ?response_type=code &client_id=operator-console &redirect_uri=https%3A%2F%2Fops.example.com%2Fcallback @@ -34,7 +34,7 @@ GET https://identity.mavula.dev/auth Exchange the returned code once: ```bash -curl -sS -X POST https://identity.mavula.dev/token \ +curl -sS -X POST https://identity.mavula.io/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=authorization_code' \ --data-urlencode 'client_id=operator-console' \ @@ -48,23 +48,45 @@ curl -sS -X POST https://identity.mavula.dev/token \ ```bash - curl -sS -X POST https://identity.mavula.dev/token \ + curl -sS -X POST https://identity.mavula.io/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id=YOUR_CLIENT_ID' \ - --data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \ - --data-urlencode 'scope=internal.worker' + --data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \ + --data-urlencode "client_assertion=$CLIENT_ASSERTION" \ + --data-urlencode 'scope=internal.worker' \ + --data-urlencode 'resource=urn:mavula:workbench' \ + --data-urlencode 'tenant_id=YOUR_TENANT_ID' ``` ```ts + import { importJWK, SignJWT } from 'jose'; + import { randomUUID } from 'node:crypto'; + + const clientId = process.env.MAVULA_CLIENT_ID!; + const tokenEndpoint = 'https://identity.mavula.io/token'; + const privateJwk = JSON.parse(process.env.MAVULA_PRIVATE_JWK_JSON!); + const signingKey = await importJWK(privateJwk, 'PS256'); + const clientAssertion = await new SignJWT({}) + .setProtectedHeader({ alg: 'PS256', kid: privateJwk.kid }) + .setIssuer(clientId) + .setSubject(clientId) + .setAudience(tokenEndpoint) + .setJti(randomUUID()) + .setIssuedAt() + .setExpirationTime('2m') + .sign(signingKey); const body = new URLSearchParams({ grant_type: 'client_credentials', - client_id: process.env.MAVULA_CLIENT_ID!, - client_secret: process.env.MAVULA_CLIENT_SECRET!, + client_id: clientId, + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion: clientAssertion, scope: 'internal.worker', + resource: 'urn:mavula:workbench', + tenant_id: process.env.MAVULA_TENANT_ID!, }); - const response = await fetch('https://identity.mavula.dev/token', { + const response = await fetch(tokenEndpoint, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body, }); if (!response.ok) throw new Error(`token request failed: ${response.status}`); @@ -73,7 +95,7 @@ curl -sS -X POST https://identity.mavula.dev/token \ -Client credentials represent an approved internal worker identity. They do not replace operator authorization for Ledger Core or compliance operations. +Client credentials represent an approved internal worker identity. Identity Access accepts only registered public RSA keys with `private_key_jwt` and `PS256`; assertions must be short-lived and use a unique `jti`. Service credentials do not replace operator authorization for Ledger Core or compliance operations. ## Refresh and revoke diff --git a/src/content/docs/v1/getting-started/quickstart.mdx b/src/content/docs/v1/getting-started/quickstart.mdx index af439c1..8023f04 100644 --- a/src/content/docs/v1/getting-started/quickstart.mdx +++ b/src/content/docs/v1/getting-started/quickstart.mdx @@ -12,7 +12,7 @@ This flow uses OAuth 2.0 authorization code with PKCE for an institutional opera 1. **Exchange the authorization code** ```bash - curl -sS -X POST https://identity.mavula.dev/token \ + curl -sS -X POST https://identity.mavula.io/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=authorization_code' \ --data-urlencode 'client_id=operator-console' \ @@ -24,7 +24,7 @@ This flow uses OAuth 2.0 authorization code with PKCE for an institutional opera 2. **Inspect the effective identity** ```bash - curl -sS https://identity.mavula.dev/api/v1/me \ + curl -sS https://identity.mavula.io/api/v1/me \ -H "Authorization: Bearer $MAVULA_ACCESS_TOKEN" ``` @@ -33,7 +33,7 @@ This flow uses OAuth 2.0 authorization code with PKCE for an institutional opera 3. **Create an account** ```bash - curl -sS -X POST https://ledger.mavula.dev/api/accounts \ + curl -sS -X POST https://ledger.mavula.io/api/accounts \ -H "Authorization: Bearer $MAVULA_ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 76b05d81-c975-4daf-b054-72b2eb3a749a' \ @@ -44,7 +44,7 @@ This flow uses OAuth 2.0 authorization code with PKCE for an institutional opera 4. **Read the balance** ```bash - curl -sS https://ledger.mavula.dev/api/accounts/ACCOUNT_ID/balance \ + curl -sS https://ledger.mavula.io/api/accounts/ACCOUNT_ID/balance \ -H "Authorization: Bearer $MAVULA_ACCESS_TOKEN" \ -H 'X-Correlation-ID: 406d8359-a53a-4d2a-a908-628798ece3b3' ``` diff --git a/src/content/docs/v1/getting-started/roles-permissions.mdx b/src/content/docs/v1/getting-started/roles-permissions.mdx index cc8023a..10d49c2 100644 --- a/src/content/docs/v1/getting-started/roles-permissions.mdx +++ b/src/content/docs/v1/getting-started/roles-permissions.mdx @@ -9,12 +9,13 @@ Authorization follows least privilege and separation of duties. A role grants a | Role | Typical responsibility | Core permissions | | --- | --- | --- | -| `operations_viewer` | Read operational and financial state | `finance.read` | +| `institution_admin` | Manage institutional identity and configuration | `identity.admin`, `configuration.write` | | `operations_maker` | Submit controlled changes | `finance.read`, `finance.write` | | `operations_checker` | Approve or reject controlled changes | `finance.read`, `finance.approve` | | `compliance_officer` | Review and generate regulated outputs | `finance.read`, `compliance.manage` | -| `platform_administrator` | Manage tenant configuration and access | Explicit administrative permissions only | -| `service_integration` | Execute a specific machine workflow | Narrow scopes for one integration | +| `auditor` | Read audit and financial evidence | `audit.read`, `finance.read` | + +`service_integration` is an OAuth client class, not an operator role. Each machine client receives only the scopes required by one integration and authenticates with `private_key_jwt`. ## Maker-checker rule @@ -23,7 +24,7 @@ The principal that submits an account lifecycle or financial adjustment request ## Verify access at runtime ```bash -curl -sS https://identity.mavula.dev/api/v1/me \ +curl -sS https://identity.mavula.io/api/v1/me \ -H "Authorization: Bearer $MAVULA_ACCESS_TOKEN" ``` diff --git a/src/content/docs/v1/guides/account-lifecycle.mdx b/src/content/docs/v1/guides/account-lifecycle.mdx index 868e507..01f6289 100644 --- a/src/content/docs/v1/guides/account-lifecycle.mdx +++ b/src/content/docs/v1/guides/account-lifecycle.mdx @@ -23,7 +23,7 @@ import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; ```bash - curl -sS -X POST https://ledger.mavula.dev/api/accounts/ACCOUNT_ID/status-transitions \ + curl -sS -X POST https://ledger.mavula.io/api/accounts/ACCOUNT_ID/status-transitions \ -H "Authorization: Bearer $MAKER_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 7f2b1e3a-4c2d-4d63-9b1a-2f7b6c0d5e91' \ @@ -83,7 +83,7 @@ import { Aside, Tabs, TabItem } from '@astrojs/starlight/components'; ## Approve as a different operator ```bash -curl -sS -X POST https://ledger.mavula.dev/api/account-lifecycle-requests/REQUEST_ID/approve \ +curl -sS -X POST https://ledger.mavula.io/api/account-lifecycle-requests/REQUEST_ID/approve \ -H "Authorization: Bearer $CHECKER_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: a65e192f-aa5f-4dc9-a111-baf6136595a0' \ diff --git a/src/content/docs/v1/guides/account-setup.mdx b/src/content/docs/v1/guides/account-setup.mdx index 64bd3cf..615e068 100644 --- a/src/content/docs/v1/guides/account-setup.mdx +++ b/src/content/docs/v1/guides/account-setup.mdx @@ -12,7 +12,7 @@ Required permission: `finance.write` to create, `finance.read` to inspect. 1. **Create the account** ```bash - curl -sS -X POST https://ledger.mavula.dev/api/accounts \ + curl -sS -X POST https://ledger.mavula.io/api/accounts \ -H "Authorization: Bearer $MAVULA_ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 56a1f534-9054-4dcf-82cb-55ebc102da18' \ @@ -23,14 +23,14 @@ Required permission: `finance.write` to create, `finance.read` to inspect. 2. **Read the current balance** ```bash - curl -sS https://ledger.mavula.dev/api/accounts/ACCOUNT_ID/balance \ + curl -sS https://ledger.mavula.io/api/accounts/ACCOUNT_ID/balance \ -H "Authorization: Bearer $MAVULA_ACCESS_TOKEN" ``` 3. **Page through the statement** ```bash - curl -sS 'https://ledger.mavula.dev/api/accounts/ACCOUNT_ID/statement?limit=100' \ + curl -sS 'https://ledger.mavula.io/api/accounts/ACCOUNT_ID/statement?limit=100' \ -H "Authorization: Bearer $MAVULA_ACCESS_TOKEN" ``` diff --git a/src/content/docs/v1/guides/configuration.mdx b/src/content/docs/v1/guides/configuration.mdx index e40545f..a817325 100644 --- a/src/content/docs/v1/guides/configuration.mdx +++ b/src/content/docs/v1/guides/configuration.mdx @@ -8,7 +8,7 @@ Configuration APIs are tenant-scoped. Apply changes first in a non-production te ## Generate jurisdiction defaults ```bash -curl -sS -X POST https://ledger.mavula.dev/api/products/config/generate \ +curl -sS -X POST https://ledger.mavula.io/api/products/config/generate \ -H "Authorization: Bearer $MAVULA_ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 4cb0de72-2805-4490-8e47-df403c118275' \ diff --git a/src/content/docs/v1/guides/financial-adjustments.mdx b/src/content/docs/v1/guides/financial-adjustments.mdx index bf0e9fb..c563ddf 100644 --- a/src/content/docs/v1/guides/financial-adjustments.mdx +++ b/src/content/docs/v1/guides/financial-adjustments.mdx @@ -8,7 +8,7 @@ Financial history is not edited in place. Submit a `REVERSAL` or `CORRECTION` ag ## Submit a reversal ```bash -curl -sS -X POST https://ledger.mavula.dev/api/financial-adjustment-requests \ +curl -sS -X POST https://ledger.mavula.io/api/financial-adjustment-requests \ -H "Authorization: Bearer $MAKER_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 7b0491f2-eb4c-42b2-b77d-acadd27c571b' \ @@ -19,7 +19,7 @@ curl -sS -X POST https://ledger.mavula.dev/api/financial-adjustment-requests \ ## Approve or reject ```bash -curl -sS -X POST https://ledger.mavula.dev/api/financial-adjustment-requests/REQUEST_ID/approve \ +curl -sS -X POST https://ledger.mavula.io/api/financial-adjustment-requests/REQUEST_ID/approve \ -H "Authorization: Bearer $CHECKER_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 147598a5-10ee-49cc-b6e5-e5415bb65844' \ diff --git a/src/content/docs/v1/guides/legacy-batches.mdx b/src/content/docs/v1/guides/legacy-batches.mdx index 45b58bb..59fe4a4 100644 --- a/src/content/docs/v1/guides/legacy-batches.mdx +++ b/src/content/docs/v1/guides/legacy-batches.mdx @@ -14,7 +14,7 @@ Workbench orchestrates auditable batch requests. Legacy imports are staged and v 1. **Request the export** ```bash - curl -sS -X POST https://workbench.mavula.dev/api/regulatory-exports \ + curl -sS -X POST https://workbench.mavula.io/api/regulatory-exports \ -H "Authorization: Bearer $COMPLIANCE_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 0e03ef93-09e0-413f-b49a-e59f93fa881f' \ @@ -28,12 +28,12 @@ Workbench orchestrates auditable batch requests. Legacy imports are staged and v 3. **Download the artifact** - Use `GET /api/legacy-batches/{batchId}/artifact` only after the batch reports a completed state. Verify the response media type and checksum before delivery. + Use `GET /api/legacy-batches/{batchId}/artifact` after an export reaches `GENERATED`. Verify the response media type and checksum before delivery. 4. **Record authority delivery** ```bash - curl -sS -X POST https://workbench.mavula.dev/api/regulatory-exports/BATCH_ID/delivery \ + curl -sS -X POST https://workbench.mavula.io/api/regulatory-exports/BATCH_ID/delivery \ -H "Authorization: Bearer $COMPLIANCE_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: 076a329b-84f5-4f58-99ef-04ac14f3a343' \ @@ -46,6 +46,20 @@ Workbench orchestrates auditable batch requests. Legacy imports are staged and v Submit the file metadata and import parameters to `POST /api/legacy-imports`. Inspect deterministic validation failures at `GET /api/legacy-batches/{batchId}/rejections`. +## Receipt states + +| State | Export | Import | Meaning | +| --- | --- | --- | --- | +| `QUEUED` | Yes | Yes | Durable receipt created; processing has not started | +| `PROCESSING` | Yes | Yes | A worker owns a fenced lease | +| `GENERATED` | Yes | No | Export artifact is available | +| `VALIDATED` | No | Yes | Import artifact passed validation | +| `REJECTED` | Yes | Yes | Deterministic business or format validation failed | +| `FAILED` | Yes | Yes | Processing exhausted its retry budget | +| `DELIVERED` | Yes | No | Authority delivery evidence was recorded idempotently | + +Retries use the same batch receipt. Lease identifiers, request digests and delivery digests are internal and are never returned by the public API. + diff --git a/src/content/docs/v1/guides/payment-jobs.mdx b/src/content/docs/v1/guides/payment-jobs.mdx index 0a35fd5..8405122 100644 --- a/src/content/docs/v1/guides/payment-jobs.mdx +++ b/src/content/docs/v1/guides/payment-jobs.mdx @@ -8,7 +8,7 @@ Workbench accepts payment jobs and delegates durable process state to the settle ## Submit a job ```bash -curl -sS -X POST https://workbench.mavula.dev/api/jobs \ +curl -sS -X POST https://workbench.mavula.io/api/jobs \ -H "Authorization: Bearer $MAVULA_ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: bfe8b575-2137-4a86-a210-cc2182130aed' \ @@ -19,7 +19,7 @@ curl -sS -X POST https://workbench.mavula.dev/api/jobs \ Persist the returned job identifier and poll the status resource: ```bash -curl -sS https://workbench.mavula.dev/api/jobs/JOB_ID \ +curl -sS https://workbench.mavula.io/api/jobs/JOB_ID \ -H "Authorization: Bearer $MAVULA_ACCESS_TOKEN" ``` diff --git a/src/content/docs/v1/index.mdx b/src/content/docs/v1/index.mdx index e10a325..942bdfe 100644 --- a/src/content/docs/v1/index.mdx +++ b/src/content/docs/v1/index.mdx @@ -9,9 +9,9 @@ MAVULA API v1 separates identity, financial state and asynchronous operations in | API | Base URL | Interaction model | | --- | --- | --- | -| Identity Access | `https://identity.mavula.dev` | OAuth 2.0 and OpenID Connect | -| Ledger Core | `https://ledger.mavula.dev` | Synchronous commands and reads | -| Workbench | `https://workbench.mavula.dev` | Asynchronous jobs and operational reads | +| Identity Access | `https://identity.mavula.io` | OAuth 2.0 and OpenID Connect | +| Ledger Core | `https://ledger.mavula.io` | Synchronous commands and reads | +| Workbench | `https://workbench.mavula.io` | Asynchronous jobs and operational reads | Tenant scope is derived from the authenticated identity. Do not send a tenant selector in the URL or body unless a specific operation schema requires it. @@ -27,4 +27,3 @@ Content-Type: application/json ``` Start with the [quickstart](./getting-started/quickstart/) or select a workflow from the sidebar. - diff --git a/src/content/docs/v1/resources/code-examples.mdx b/src/content/docs/v1/resources/code-examples.mdx index b0d8ee0..65d4be9 100644 --- a/src/content/docs/v1/resources/code-examples.mdx +++ b/src/content/docs/v1/resources/code-examples.mdx @@ -19,7 +19,7 @@ The guides include executable fragments for curl, TypeScript, Python, Java and G ## Postman -The repository includes a versioned collection at [`examples/postman/MAVULA-API-v1.postman_collection.json`](https://github.com/mavulahq/developer-docs/blob/main/examples/postman/MAVULA-API-v1.postman_collection.json). Define `identity_base_url`, `ledger_base_url`, `workbench_base_url`, `client_id`, `client_secret` and `access_token` in a private Postman environment. +The repository includes a versioned collection at [`examples/postman/MAVULA-API-v1.postman_collection.json`](https://github.com/mavulahq/developer-docs/blob/main/examples/postman/MAVULA-API-v1.postman_collection.json). Define `identity_base_url`, `ledger_base_url`, `workbench_base_url`, `client_id`, `client_assertion` and `access_token` in a private Postman environment. Generate `client_assertion` outside the collection from the registered private RSA key; never store the key in Postman. ## Reference contracts @@ -28,4 +28,3 @@ The repository includes a versioned collection at [`examples/postman/MAVULA-API- - [Workbench OpenAPI](/developer-docs/openapi/workbench.public.v1.yaml) Use these contracts to generate clients in your preferred language. Pin the downloaded contract digest in regulated build pipelines. - diff --git a/test/contracts.test.mjs b/test/contracts.test.mjs index 64855f4..e69cf11 100644 --- a/test/contracts.test.mjs +++ b/test/contracts.test.mjs @@ -46,8 +46,8 @@ test('published examples use canonical hosts and contract payload names', () => 'examples/postman/MAVULA-API-v1.postman_collection.json', ]; const content = files.map((file) => readFileSync(file, 'utf8')).join('\n'); - assert.doesNotMatch(content, /https:\/\/(?:identity|ledger|workbench)\.mavula\.io/); - for (const expectation of ['mavula.dev', 'compliance.manage', 'valueMinor', 'period_from', 'authority_reference']) { + assert.doesNotMatch(content, /https:\/\/(?:identity|ledger|workbench)\.mavula\.dev/); + for (const expectation of ['mavula.io', 'compliance.manage', 'valueMinor', 'period_from', 'authority_reference']) { assert.match(content, new RegExp(expectation, 'i')); } });