@mettlecast/domain-runtime 0.2.59 → 0.2.61
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/README.md +57 -40
- package/dist/primitives/action.d.ts +80 -8
- package/dist/primitives/action.js +71 -4
- package/dist/primitives/index.d.ts +0 -1
- package/dist/primitives/index.js +0 -1
- package/dist/runtime/action-executor.d.ts +164 -0
- package/dist/runtime/action-executor.js +408 -0
- package/dist/runtime/action-handler.d.ts +110 -6
- package/dist/runtime/action-handler.js +110 -10
- package/dist/runtime/actions.d.ts +65 -5
- package/dist/runtime/actions.js +118 -13
- package/dist/runtime/audit.d.ts +23 -3
- package/dist/runtime/audit.js +20 -3
- package/dist/runtime/db.d.ts +23 -4
- package/dist/runtime/db.js +27 -6
- package/dist/runtime/error-formatter.d.ts +63 -0
- package/dist/runtime/error-formatter.js +68 -1
- package/dist/runtime/exposed-action-api-handler.d.ts +87 -0
- package/dist/runtime/exposed-action-api-handler.js +287 -0
- package/dist/runtime/extractors.d.ts +12 -0
- package/dist/runtime/extractors.js +21 -13
- package/dist/runtime/files.d.ts +8 -0
- package/dist/runtime/files.js +76 -8
- package/dist/runtime/hydrate.js +17 -2
- package/dist/runtime/index.d.ts +10 -2
- package/dist/runtime/index.js +18 -1
- package/dist/runtime/observability-bindings.d.ts +110 -0
- package/dist/runtime/observability-bindings.js +94 -0
- package/dist/runtime/store.d.ts +8 -0
- package/dist/runtime/store.js +29 -0
- package/dist/runtime/tracer.d.ts +1 -1
- package/dist/runtime/tracer.js +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/rls.d.ts +31 -0
- package/dist/schema/rls.js +44 -0
- package/dist/types/tenant.d.ts +12 -0
- package/package.json +1 -1
- package/dist/primitives/api.d.ts +0 -44
- package/dist/primitives/api.js +0 -70
- package/dist/runtime/api-handler.d.ts +0 -29
- package/dist/runtime/api-handler.js +0 -212
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @mettlecast/domain-runtime
|
|
2
2
|
|
|
3
|
-
Type-safe runtime types, factory functions, and context interface for TIB
|
|
3
|
+
Type-safe runtime types, factory functions, and context interface for TIB
|
|
4
|
+
Domain Module handlers. Zero AWS dependencies in the main export — all
|
|
5
|
+
infrastructure concerns are delegated to the CDK packer.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -10,14 +12,13 @@ npm install @mettlecast/domain-runtime
|
|
|
10
12
|
|
|
11
13
|
## Quick Start
|
|
12
14
|
|
|
13
|
-
Define a domain with
|
|
15
|
+
Define a domain with a public action handler:
|
|
14
16
|
|
|
15
17
|
```typescript
|
|
16
18
|
import {
|
|
17
19
|
defineDomain,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
type DomainContext,
|
|
20
|
+
defineAction,
|
|
21
|
+
z,
|
|
21
22
|
} from '@mettlecast/domain-runtime';
|
|
22
23
|
|
|
23
24
|
const domain = defineDomain({
|
|
@@ -25,74 +26,90 @@ const domain = defineDomain({
|
|
|
25
26
|
version: '1.0.0',
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
// Public HTTP endpoint — equivalent of the legacy `defineApi({ tenancy: 'required' })`.
|
|
30
|
+
// The `exposure` block is mandatory: path/method/auth/tenancy are validated at
|
|
31
|
+
// construction time and at registry build, so unsafe configurations never ship.
|
|
32
|
+
export const chargeCard = defineAction({
|
|
33
|
+
id: 'charge-card',
|
|
34
|
+
backendAccess: 'domain',
|
|
35
|
+
exposure: {
|
|
36
|
+
type: 'api',
|
|
37
|
+
path: '/v1/tenants/{tenantId}/payments/charge',
|
|
38
|
+
method: 'POST',
|
|
39
|
+
auth: 'required',
|
|
40
|
+
tenancy: 'required',
|
|
36
41
|
},
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
path: '/charge',
|
|
42
|
-
method: 'POST',
|
|
43
|
-
handler: async (event, ctx: DomainContext) => {
|
|
44
|
-
const amount = event.body.amount;
|
|
45
|
-
|
|
46
|
-
// Use context surfaces
|
|
42
|
+
input: z.object({ amount: z.number().positive() }),
|
|
43
|
+
output: z.object({ id: z.string().uuid(), status: z.literal('charged') }),
|
|
44
|
+
idempotent: true,
|
|
45
|
+
handler: async (input, ctx) => {
|
|
47
46
|
await ctx.db.query('INSERT INTO charges ...');
|
|
48
|
-
await ctx.publish(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return { statusCode: 200, body: { success: true } };
|
|
47
|
+
await ctx.publish('payment.charged', { amount: input.amount });
|
|
48
|
+
return { id: crypto.randomUUID(), status: 'charged' as const };
|
|
52
49
|
},
|
|
53
50
|
});
|
|
54
51
|
```
|
|
55
52
|
|
|
53
|
+
The action-first contract (introduced in #4619, hardened in
|
|
54
|
+
`feat/4625-action-auth-hardening`):
|
|
55
|
+
|
|
56
|
+
| `exposure.type` | Reachable through | Notes |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `'api'` | API Gateway route | Path, method, auth, tenancy all required and validated. Generates one method on the consumer SDK. |
|
|
59
|
+
| `'internal'` | `ctx.actions[domainId].<id>(input)` from inside the platform | Never reachable via HTTP. Use for in-process cross-domain calls. |
|
|
60
|
+
|
|
61
|
+
> **Anti-pattern.** Do NOT use the legacy `defineApi` factory for new
|
|
62
|
+
> public handlers. It is no longer scaffolded for public HTTP endpoints —
|
|
63
|
+
> every public handler must go through `defineAction({ exposure: { type: 'api', ... } })`
|
|
64
|
+
> so the registry can enforce the auth/tenancy contract at build time
|
|
65
|
+
> (see `validate-domain`).
|
|
66
|
+
|
|
56
67
|
## Factory Functions
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
Nine factory functions help you define domain primitives with schema
|
|
70
|
+
validation and type safety. The action-first migration (#4619) folded
|
|
71
|
+
`defineApi` into `defineAction` so every primitive is a callable
|
|
72
|
+
`ActionDefinition` regardless of where it is reached from.
|
|
59
73
|
|
|
60
74
|
| Function | Purpose | Produces |
|
|
61
75
|
|---|---|---|
|
|
62
76
|
| `defineDomain()` | Declare a domain and version | Domain metadata |
|
|
63
|
-
| `
|
|
77
|
+
| `defineAction()` | Public API endpoint OR internal callable | Action handler + (optional) API Gateway route |
|
|
64
78
|
| `defineEvent()` | Event type with schema | Typed event publisher |
|
|
65
|
-
| `defineWebhook()` | Inbound
|
|
79
|
+
| `defineWebhook()` | Inbound webhook handler | Webhook + validation |
|
|
66
80
|
| `defineSubscriber()` | Event subscriber handler | EventBridge rule + Lambda |
|
|
67
81
|
| `defineSchedule()` | Cron-triggered handler | EventBridge Scheduler rule |
|
|
68
82
|
| `defineJob()` | Async queue-based task | SQS queue + Lambda consumer |
|
|
69
|
-
| `defineAction()` | Flow Designer action | Workspace-invoked handler |
|
|
70
83
|
| `defineIntegration()` | External service integration | Async integration handler |
|
|
84
|
+
| `defineFlow()` | Multi-step orchestration flow | Step Functions state machine |
|
|
71
85
|
|
|
72
86
|
## Context Surfaces
|
|
73
87
|
|
|
74
|
-
|
|
88
|
+
The unified `DomainContext` injected into every handler exposes 14
|
|
89
|
+
surfaces:
|
|
75
90
|
|
|
76
91
|
| Surface | Purpose |
|
|
77
92
|
|---|---|
|
|
78
|
-
| `ctx.db` | PostgreSQL connection pool via drizzle-orm |
|
|
93
|
+
| `ctx.db` | PostgreSQL connection pool via drizzle-orm (auto tenant-scoped) |
|
|
79
94
|
| `ctx.publish()` | Publish an event to EventBridge |
|
|
80
|
-
| `ctx.actions` | Invoke other domain actions |
|
|
95
|
+
| `ctx.actions` | Invoke other domain actions (`ctx.actions[domainId].<id>(input)`) |
|
|
81
96
|
| `ctx.integrations` | Call external integrations |
|
|
82
97
|
| `ctx.jobs` | Enqueue async tasks to SQS |
|
|
83
|
-
| `ctx.flows` | Trigger
|
|
84
|
-
| `ctx.cache` | In-memory or distributed cache
|
|
98
|
+
| `ctx.flows` | Trigger multi-step flows |
|
|
99
|
+
| `ctx.cache` | In-memory or distributed cache |
|
|
85
100
|
| `ctx.secrets` | Fetch AWS Secrets Manager values |
|
|
86
|
-
| `ctx.fetch()` | HTTP client with
|
|
101
|
+
| `ctx.fetch()` | HTTP client with retry + circuit breaker |
|
|
87
102
|
| `ctx.idempotency` | Deduplication by request ID |
|
|
88
103
|
| `ctx.logger` | Pino JSON logger |
|
|
89
104
|
| `ctx.tracer` | AWS X-Ray tracing |
|
|
90
|
-
| `ctx.
|
|
91
|
-
| `ctx.
|
|
105
|
+
| `ctx.actor` | Caller identity (sub, email, tenantId, roles, scopes) |
|
|
106
|
+
| `ctx.tenant` | Resolved tenant (`{ id, workspaceId, orgId }`) |
|
|
92
107
|
|
|
93
108
|
## Design Principles
|
|
94
109
|
|
|
95
|
-
This package is **intentionally framework-agnostic**. It exports types
|
|
110
|
+
This package is **intentionally framework-agnostic**. It exports types
|
|
111
|
+
and factory functions only — all infrastructure (Lambdas, API Gateway,
|
|
112
|
+
EventBridge, SQS, etc.) is provisioned by `@mettlecast/domain-cdk-packer`.
|
|
96
113
|
|
|
97
114
|
**Exports by concern:**
|
|
98
115
|
|
|
@@ -1,36 +1,108 @@
|
|
|
1
1
|
import { type ZodSchema } from 'zod';
|
|
2
2
|
import type { DomainContext } from '../ctx/context.js';
|
|
3
3
|
import type { OutboundAccess } from '../types/index.js';
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Backend access scope for an action.
|
|
6
|
+
* - `private` — callable only inside the defining domain.
|
|
7
|
+
* - `domain` — callable by any domain in the same workspace.
|
|
8
|
+
* - `platform` — callable by platform-level services (cross-workspace).
|
|
9
|
+
*/
|
|
10
|
+
export type BackendAccess = 'private' | 'domain' | 'platform';
|
|
11
|
+
/**
|
|
12
|
+
* Authentication posture for an API-exposed action.
|
|
13
|
+
* - `required` — caller's identity must be authenticated.
|
|
14
|
+
* - `none` — anonymous endpoint; requires a `securityException.reason`.
|
|
15
|
+
* - `service` — service-to-service only (no end-user identity).
|
|
16
|
+
*/
|
|
17
|
+
export type ApiAuthMode = 'required' | 'none' | 'service';
|
|
18
|
+
/**
|
|
19
|
+
* Tenancy posture for an API-exposed action.
|
|
20
|
+
*/
|
|
21
|
+
export type ApiTenancyMode = 'required' | 'none' | 'system';
|
|
22
|
+
/**
|
|
23
|
+
* Recorded justification for relaxing the default auth/tenancy posture.
|
|
24
|
+
* Required when `auth: 'none'` and when `tenancy: 'system'` is combined with
|
|
25
|
+
* `auth: 'required'` without role narrowing.
|
|
26
|
+
*/
|
|
27
|
+
export interface SecurityException {
|
|
28
|
+
/** Human-readable reason captured alongside the primitive. */
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Action reachable only through in-process proxies (no HTTP route).
|
|
33
|
+
* Carries no path, method, auth, or tenancy fields.
|
|
34
|
+
*/
|
|
35
|
+
export interface InternalExposure {
|
|
36
|
+
type: 'internal';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Action exposed as an HTTP endpoint. The downstream CDK/registry
|
|
40
|
+
* pipeline generates API Gateway routes from this shape.
|
|
41
|
+
*/
|
|
42
|
+
export interface ApiExposure {
|
|
43
|
+
type: 'api';
|
|
44
|
+
/** HTTP route path relative to the domain base. Must start with `/`. */
|
|
45
|
+
path: string;
|
|
46
|
+
/** Explicit HTTP method. Never `ANY`. */
|
|
47
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
48
|
+
/** Authentication posture for the HTTP route. */
|
|
49
|
+
auth: ApiAuthMode;
|
|
50
|
+
/** Tenancy posture for the HTTP route. */
|
|
51
|
+
tenancy: ApiTenancyMode;
|
|
52
|
+
/** Required role claims (only meaningful when `auth` is `required`). */
|
|
53
|
+
roles?: string[];
|
|
54
|
+
/** Required justification when the auth/tenancy posture is relaxed. */
|
|
55
|
+
securityException?: SecurityException;
|
|
56
|
+
}
|
|
57
|
+
/** Discriminated union of every legal exposure for an action. */
|
|
58
|
+
export type ActionExposure = InternalExposure | ApiExposure;
|
|
6
59
|
/** Configuration for an action primitive. */
|
|
7
60
|
export interface ActionConfig {
|
|
8
61
|
/** Machine-readable identifier (kebab-case). */
|
|
9
62
|
id: string;
|
|
10
|
-
/**
|
|
11
|
-
|
|
63
|
+
/** Backend access scope. `private` = only within the same domain. */
|
|
64
|
+
backendAccess: BackendAccess;
|
|
65
|
+
/** How this action is reachable — internal proxy or HTTP route. */
|
|
66
|
+
exposure: ActionExposure;
|
|
12
67
|
/** Zod schema for validated input. */
|
|
13
68
|
input: ZodSchema<any>;
|
|
14
69
|
/** Zod schema for return type. */
|
|
15
70
|
output: ZodSchema<any>;
|
|
16
71
|
/**
|
|
17
72
|
* Whether to enforce idempotency for this action.
|
|
18
|
-
* If true, the runtime checks ctx.idempotency before executing.
|
|
73
|
+
* If true, the runtime checks `ctx.idempotency` before executing.
|
|
19
74
|
*/
|
|
20
75
|
idempotent?: boolean;
|
|
21
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Optional caller allowlist for sensitive `backendAccess: 'platform'`
|
|
78
|
+
* actions. When present and non-empty, `executeAction` denies internal
|
|
79
|
+
* callers whose `callerDomain` is not in the list. This narrows broad
|
|
80
|
+
* platform-scoped primitives — for example, an internal "rotate
|
|
81
|
+
* encryption key" action — without forcing every caller into the more
|
|
82
|
+
* restrictive `private`/`domain` scopes.
|
|
83
|
+
*
|
|
84
|
+
* - `undefined` or `[]` — fall back to the `backendAccess` policy.
|
|
85
|
+
* - non-empty list — callerDomain MUST appear in the list.
|
|
86
|
+
*
|
|
87
|
+
* API-exposed calls (`source.type === 'api'`) skip this check; the
|
|
88
|
+
* allowlist only governs the in-process / cross-Lambda proxy path,
|
|
89
|
+
* which is the channel a runaway backend can otherwise exploit.
|
|
90
|
+
*/
|
|
91
|
+
allowedCallers?: string[];
|
|
92
|
+
/** Whether this handler may use NAT-backed public internet egress. Defaults to `internal`. */
|
|
22
93
|
outboundAccess?: OutboundAccess;
|
|
23
94
|
/** Handler implementation. */
|
|
24
95
|
handler: (input: any, ctx: DomainContext) => Promise<any>;
|
|
25
96
|
}
|
|
26
|
-
/** Return type of defineAction
|
|
97
|
+
/** Return type of `defineAction`. */
|
|
27
98
|
export interface ActionDefinition extends ActionConfig {
|
|
28
99
|
/** Discriminant. */
|
|
29
100
|
_kind: 'action';
|
|
30
101
|
}
|
|
31
102
|
/**
|
|
32
103
|
* Register a callable domain action.
|
|
33
|
-
*
|
|
104
|
+
* The runtime validates `backendAccess` and the discriminated `exposure`
|
|
105
|
+
* against the action-first contract before accepting the primitive.
|
|
34
106
|
* @throws {ZodError} if config is invalid
|
|
35
107
|
*/
|
|
36
108
|
export declare function defineAction(config: ActionConfig): ActionDefinition;
|
|
@@ -1,16 +1,83 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
// Native enum objects — Zod v4's recommended pattern over `z.enum([...])`.
|
|
3
|
+
// Each `as const` object is the source of truth; `z.nativeEnum(...)` derives
|
|
4
|
+
// the inferred string-literal union from the keys.
|
|
5
|
+
const BackendAccessLiteral = { private: 'private', domain: 'domain', platform: 'platform' };
|
|
6
|
+
const ApiAuthModeLiteral = { required: 'required', none: 'none', service: 'service' };
|
|
7
|
+
const ApiTenancyModeLiteral = { required: 'required', none: 'none', system: 'system' };
|
|
8
|
+
const ApiMethodLiteral = {
|
|
9
|
+
GET: 'GET',
|
|
10
|
+
POST: 'POST',
|
|
11
|
+
PUT: 'PUT',
|
|
12
|
+
PATCH: 'PATCH',
|
|
13
|
+
DELETE: 'DELETE',
|
|
14
|
+
HEAD: 'HEAD',
|
|
15
|
+
OPTIONS: 'OPTIONS',
|
|
16
|
+
};
|
|
17
|
+
// Internal exposure — only `type` is allowed; any additional field is a bug.
|
|
18
|
+
const InternalExposureSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
type: z.literal('internal'),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
// API exposure — required fields present, optional fields allowed, then
|
|
24
|
+
// three cross-field refinements encode the action-first contract.
|
|
25
|
+
const ApiExposureSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
type: z.literal('api'),
|
|
28
|
+
path: z.string().startsWith('/'),
|
|
29
|
+
method: z.nativeEnum(ApiMethodLiteral),
|
|
30
|
+
auth: z.nativeEnum(ApiAuthModeLiteral),
|
|
31
|
+
tenancy: z.nativeEnum(ApiTenancyModeLiteral),
|
|
32
|
+
roles: z.array(z.string().min(1)).optional(),
|
|
33
|
+
securityException: z.object({ reason: z.string().min(1) }).optional(),
|
|
34
|
+
})
|
|
35
|
+
.strict()
|
|
36
|
+
// auth: 'none' requires a non-empty securityException.reason
|
|
37
|
+
.refine((e) => e.auth !== 'none' || (e.securityException?.reason?.length ?? 0) > 0, {
|
|
38
|
+
message: "exposure.auth 'none' requires exposure.securityException.reason",
|
|
39
|
+
path: ['securityException'],
|
|
40
|
+
})
|
|
41
|
+
// tenancy: 'required' → path must include '/v1/tenants/{tenantId}/'
|
|
42
|
+
.refine((e) => e.tenancy !== 'required' || e.path.includes('/v1/tenants/{tenantId}/'), {
|
|
43
|
+
message: "exposure.tenancy 'required' requires exposure.path to include '/v1/tenants/{tenantId}/'",
|
|
44
|
+
path: ['path'],
|
|
45
|
+
})
|
|
46
|
+
// tenancy: 'system' requires auth 'required' or 'service'. When auth is
|
|
47
|
+
// 'service' the exposure is service-only and no roles are required;
|
|
48
|
+
// when auth is 'required' a non-empty roles array is mandatory.
|
|
49
|
+
.refine((e) => {
|
|
50
|
+
if (e.tenancy !== 'system')
|
|
51
|
+
return true;
|
|
52
|
+
if (e.auth !== 'required' && e.auth !== 'service')
|
|
53
|
+
return false;
|
|
54
|
+
if (e.auth === 'service')
|
|
55
|
+
return true;
|
|
56
|
+
return Array.isArray(e.roles) && e.roles.length > 0;
|
|
57
|
+
}, {
|
|
58
|
+
message: "exposure.tenancy 'system' requires exposure.auth 'required' or 'service'; when auth is 'required' a non-empty roles array is mandatory",
|
|
59
|
+
path: ['tenancy'],
|
|
60
|
+
});
|
|
61
|
+
const ActionExposureSchema = z.discriminatedUnion('type', [
|
|
62
|
+
InternalExposureSchema,
|
|
63
|
+
ApiExposureSchema,
|
|
64
|
+
]);
|
|
65
|
+
const ZodSchemaRefinement = z.custom((v) => typeof v === 'object' && v !== null && typeof v.parse === 'function', { message: 'input/output must be Zod schema objects' });
|
|
2
66
|
const ActionConfigSchema = z.object({
|
|
3
67
|
id: z.string().min(1),
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
68
|
+
backendAccess: z.nativeEnum(BackendAccessLiteral),
|
|
69
|
+
exposure: ActionExposureSchema,
|
|
70
|
+
input: ZodSchemaRefinement,
|
|
71
|
+
output: ZodSchemaRefinement,
|
|
7
72
|
idempotent: z.boolean().optional(),
|
|
73
|
+
allowedCallers: z.array(z.string().min(1)).optional(),
|
|
8
74
|
outboundAccess: z.enum(['internal', 'internet']).default('internal'),
|
|
9
75
|
handler: z.function(),
|
|
10
76
|
});
|
|
11
77
|
/**
|
|
12
78
|
* Register a callable domain action.
|
|
13
|
-
*
|
|
79
|
+
* The runtime validates `backendAccess` and the discriminated `exposure`
|
|
80
|
+
* against the action-first contract before accepting the primitive.
|
|
14
81
|
* @throws {ZodError} if config is invalid
|
|
15
82
|
*/
|
|
16
83
|
export function defineAction(config) {
|
package/dist/primitives/index.js
CHANGED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared action executor.
|
|
3
|
+
*
|
|
4
|
+
* Wave 2 of #4619 — used by both API-exposed actions and internal action
|
|
5
|
+
* invocations. Centralises the validation, auth/tenant checks, idempotency,
|
|
6
|
+
* audit, handler execution, output validation, and DB cleanup steps so the
|
|
7
|
+
* downstream API and internal adapters can stay thin and uniform.
|
|
8
|
+
*
|
|
9
|
+
* This module does NOT format HTTP responses — transport-specific adapters
|
|
10
|
+
* own response shaping. The executor returns parsed output (or throws a
|
|
11
|
+
* `TibError`) and is responsible only for the in-process pipeline.
|
|
12
|
+
*/
|
|
13
|
+
import { ZodError } from 'zod';
|
|
14
|
+
import type { DomainContext } from '../ctx/context.js';
|
|
15
|
+
import type { ActionDefinition, BackendAccess } from '../primitives/action.js';
|
|
16
|
+
import { TibError } from './error-formatter.js';
|
|
17
|
+
/** Action invocation originating from an API Gateway route. */
|
|
18
|
+
export interface ApiActionSource {
|
|
19
|
+
type: 'api';
|
|
20
|
+
/**
|
|
21
|
+
* Tenant ID extracted from the route path (e.g. `/v1/tenants/{tenantId}/...`).
|
|
22
|
+
* When provided, must match `ctx.tenant.id`. Optional to keep the executor
|
|
23
|
+
* usable when path-param extraction is delegated to the adapter.
|
|
24
|
+
*/
|
|
25
|
+
pathTenantId?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Role claims attached to the inbound request (e.g. parsed from JWT scopes
|
|
28
|
+
* or custom claims). Only consulted when the action's exposure requires
|
|
29
|
+
* roles (auth 'required' + tenancy 'system').
|
|
30
|
+
*/
|
|
31
|
+
actorRoles?: string[];
|
|
32
|
+
}
|
|
33
|
+
/** Action invocation originating from a same-process or cross-process backend call. */
|
|
34
|
+
export interface InternalActionSource {
|
|
35
|
+
type: 'internal';
|
|
36
|
+
/** Domain ID of the caller. Used for backendAccess enforcement. */
|
|
37
|
+
callerDomain?: string;
|
|
38
|
+
/** Tenant ID the caller claims to operate under (must match ctx.tenant.id). */
|
|
39
|
+
callerTenantId?: string;
|
|
40
|
+
}
|
|
41
|
+
/** Discriminated union of every legal invocation source for an action. */
|
|
42
|
+
export type ActionSource = ApiActionSource | InternalActionSource;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve whether a given caller domain may invoke an action based on its
|
|
45
|
+
* declared `backendAccess` scope and optional `allowedCallers` allowlist.
|
|
46
|
+
*
|
|
47
|
+
* Rules:
|
|
48
|
+
* - `allowedCallers` (if non-empty) takes precedence: callerDomain MUST be
|
|
49
|
+
* in the list regardless of `backendAccess`. Used to narrow broad
|
|
50
|
+
* platform-scoped primitives for sensitive handlers (issue #4662,
|
|
51
|
+
* Task A hardening).
|
|
52
|
+
* - `'private'` — only the defining domain may call it. When
|
|
53
|
+
* `definingDomain` is missing we fail CLOSED (deny) —
|
|
54
|
+
* the in-process proxy must always pass defining-domain
|
|
55
|
+
* metadata now that Wave 7 Task 7.1 has plumbed it
|
|
56
|
+
* through `createActionsProxy` / `invokeInProcess`.
|
|
57
|
+
* - `'domain'` — any caller whose domain is non-platform is allowed (the
|
|
58
|
+
* callerDomain is just recorded for audit; the policy here
|
|
59
|
+
* is "callable by other domains").
|
|
60
|
+
* - `'platform'` — reserved for platform-level services. We currently allow
|
|
61
|
+
* any non-undefined callerDomain through; finer-grained
|
|
62
|
+
* allowlists are out of scope for Wave 2 and will be wired
|
|
63
|
+
* in once the action registry exposes the defining domain.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isCallerAllowed(opts: {
|
|
66
|
+
action: Pick<ActionDefinition, 'id' | 'backendAccess' | 'allowedCallers'>;
|
|
67
|
+
callerDomain?: string;
|
|
68
|
+
definingDomain?: string;
|
|
69
|
+
}): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Throws `TibError` when `condition` is falsy. Used to enforce preconditions
|
|
72
|
+
* before any handler work begins; the resulting error carries a stable `code`
|
|
73
|
+
* so adapters can map it to the appropriate HTTP status without parsing
|
|
74
|
+
* the message.
|
|
75
|
+
*/
|
|
76
|
+
export declare function assertOrThrow(condition: unknown, message: string, code: string): asserts condition;
|
|
77
|
+
/**
|
|
78
|
+
* Lightweight tenant scoping check. Centralised so the API and internal
|
|
79
|
+
* adapters share the same wording and codes.
|
|
80
|
+
*
|
|
81
|
+
* - `ctx.tenant.id` must be a non-empty string other than `'unknown'`.
|
|
82
|
+
* - When `expectedTenantId` is provided (path or caller claim), it must
|
|
83
|
+
* match `ctx.tenant.id`.
|
|
84
|
+
*/
|
|
85
|
+
export declare function ensureTenantMatches(ctx: DomainContext, expectedTenantId?: string): void;
|
|
86
|
+
/**
|
|
87
|
+
* Check role claims against an action's required role list. Only meaningful
|
|
88
|
+
* when `auth === 'required'` AND `tenancy === 'system'`.
|
|
89
|
+
*
|
|
90
|
+
* Returns `true` when `requiredRoles` is empty/undefined (no narrowing).
|
|
91
|
+
*/
|
|
92
|
+
export declare function actorHasRequiredRoles(actorRoles: string[] | undefined, requiredRoles: string[] | undefined): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Optional metadata that adapters may attach so audit/log lines can carry
|
|
95
|
+
* `traceId`, `domainId`, and the action's defining-domain identifier. The
|
|
96
|
+
* executor never requires these — when omitted it falls back to safe
|
|
97
|
+
* defaults — but supplying them produces much richer observability.
|
|
98
|
+
*/
|
|
99
|
+
export interface ExecuteActionMeta {
|
|
100
|
+
/** Domain ID that owns the action being executed. */
|
|
101
|
+
definingDomain?: string;
|
|
102
|
+
/** Trace ID for the current request. */
|
|
103
|
+
traceId?: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Result envelope returned by `executeAction`. Adapters translate this into
|
|
107
|
+
* transport-specific responses (HTTP status, Lambda payload, etc.).
|
|
108
|
+
*
|
|
109
|
+
* - `ok: true` — handler returned a value that passed the output schema.
|
|
110
|
+
* - `ok: false` — the failure carried a domain-meaningful error code that
|
|
111
|
+
* the adapter can map to a status. The executor never returns HTTP
|
|
112
|
+
* response objects.
|
|
113
|
+
*/
|
|
114
|
+
export type ExecuteActionResult = {
|
|
115
|
+
ok: true;
|
|
116
|
+
value: unknown;
|
|
117
|
+
replayed: boolean;
|
|
118
|
+
} | {
|
|
119
|
+
ok: false;
|
|
120
|
+
error: TibError | ZodError;
|
|
121
|
+
code: string;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Run an action through the shared pipeline.
|
|
125
|
+
*
|
|
126
|
+
* Pipeline:
|
|
127
|
+
* 1. Validate input with `action.input.parse(input)`.
|
|
128
|
+
* 2. Enforce auth/tenancy preconditions based on `source`.
|
|
129
|
+
* 3. Enforce `backendAccess` for internal calls.
|
|
130
|
+
* 4. Check `ctx.idempotency` when `action.idempotent` is true.
|
|
131
|
+
* 5. Invoke `action.handler(parsedInput, ctx)`.
|
|
132
|
+
* 6. Validate output with `action.output.parse(result)`.
|
|
133
|
+
* 7. Mark idempotency after successful handler execution.
|
|
134
|
+
* 8. Audit-log the invocation.
|
|
135
|
+
* 9. Release `ctx.db` (only if ownership flag is set).
|
|
136
|
+
*
|
|
137
|
+
* The executor does NOT throw for application-level failures — it returns
|
|
138
|
+
* a `TibError`-bearing `ExecuteActionResult` so the caller can render it
|
|
139
|
+
* consistently. Truly unexpected throws propagate to the caller.
|
|
140
|
+
*/
|
|
141
|
+
export declare function executeAction(action: ActionDefinition, input: unknown, ctx: DomainContext, source: ActionSource, meta?: ExecuteActionMeta): Promise<ExecuteActionResult>;
|
|
142
|
+
/**
|
|
143
|
+
* Release the DB client associated with `ctx`. Safe to call multiple times —
|
|
144
|
+
* each `DbContext.release()` implementation is idempotent (see `createDb`).
|
|
145
|
+
*
|
|
146
|
+
* The executor does NOT auto-release by default because most adapters
|
|
147
|
+
* (api-handler, action-handler) already own the DB lifecycle for the
|
|
148
|
+
* surrounding request scope. Use `withActionExecution` when the executor
|
|
149
|
+
* is the top-level owner of the request (e.g. standalone scripts, tests,
|
|
150
|
+
* or any future one-shot action runner).
|
|
151
|
+
*/
|
|
152
|
+
export declare function releaseDbSafely(ctx: DomainContext): Promise<void>;
|
|
153
|
+
/**
|
|
154
|
+
* Wrap `executeAction` so the caller can opt the executor into owning the
|
|
155
|
+
* DB lifecycle. When `ownDbLifecycle: true` the executor runs `ctx.db.release()`
|
|
156
|
+
* in a `finally` block. Existing adapters (api-handler, action-handler)
|
|
157
|
+
* continue to own the DB themselves and should call `executeAction` directly
|
|
158
|
+
* — there is no double-release because the existing `createDb`/`createMockDb`
|
|
159
|
+
* release paths are themselves idempotent.
|
|
160
|
+
*/
|
|
161
|
+
export declare function withActionExecution(action: ActionDefinition, input: unknown, ctx: DomainContext, source: ActionSource, meta?: ExecuteActionMeta, options?: {
|
|
162
|
+
ownDbLifecycle?: boolean;
|
|
163
|
+
}): Promise<ExecuteActionResult>;
|
|
164
|
+
export type { BackendAccess };
|