@entifix/service-shell 0.8.0 → 0.10.0
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 +7 -7
- package/dist/auth/failures.d.ts +23 -0
- package/dist/auth/failures.d.ts.map +1 -0
- package/dist/auth/failures.js +20 -0
- package/dist/auth/failures.js.map +1 -0
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/require-organization.d.ts +1 -2
- package/dist/auth/require-organization.d.ts.map +1 -1
- package/dist/auth/require-organization.js +2 -7
- package/dist/auth/require-organization.js.map +1 -1
- package/dist/auth/require-principal.d.ts +3 -3
- package/dist/auth/require-principal.d.ts.map +1 -1
- package/dist/auth/require-principal.js +6 -14
- package/dist/auth/require-principal.js.map +1 -1
- package/dist/auth/require-service-crossing.d.ts +3 -3
- package/dist/auth/require-service-crossing.d.ts.map +1 -1
- package/dist/auth/require-service-crossing.js +18 -21
- package/dist/auth/require-service-crossing.js.map +1 -1
- package/dist/auth/shared-token.d.ts +2 -2
- package/dist/auth/shared-token.d.ts.map +1 -1
- package/dist/auth/shared-token.js +7 -8
- package/dist/auth/shared-token.js.map +1 -1
- package/dist/entity-facet-routes.d.ts +38 -0
- package/dist/entity-facet-routes.d.ts.map +1 -0
- package/dist/entity-facet-routes.js +67 -0
- package/dist/entity-facet-routes.js.map +1 -0
- package/dist/entity-metadata-route.d.ts +1 -1
- package/dist/entity-metadata-route.d.ts.map +1 -1
- package/dist/entity-metadata-route.js +6 -9
- package/dist/entity-metadata-route.js.map +1 -1
- package/dist/entity-route-guard.d.ts +60 -0
- package/dist/entity-route-guard.d.ts.map +1 -0
- package/dist/entity-route-guard.js +29 -0
- package/dist/entity-route-guard.js.map +1 -0
- package/dist/entity-routes.d.ts +17 -61
- package/dist/entity-routes.d.ts.map +1 -1
- package/dist/entity-routes.js +19 -77
- package/dist/entity-routes.js.map +1 -1
- package/dist/health-routes.d.ts.map +1 -1
- package/dist/health-routes.js +3 -2
- package/dist/health-routes.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/make-service.d.ts.map +1 -1
- package/dist/make-service.js +4 -2
- package/dist/make-service.js.map +1 -1
- package/dist/respond.d.ts +92 -0
- package/dist/respond.d.ts.map +1 -0
- package/dist/respond.js +178 -0
- package/dist/respond.js.map +1 -0
- package/dist/route-responses.d.ts +44 -0
- package/dist/route-responses.d.ts.map +1 -0
- package/dist/route-responses.js +104 -0
- package/dist/route-responses.js.map +1 -0
- package/dist/service-description-route.d.ts +1 -1
- package/dist/service-description-route.d.ts.map +1 -1
- package/dist/service-description-route.js +2 -1
- package/dist/service-description-route.js.map +1 -1
- package/package.json +12 -12
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/respond.ts"],"sourcesContent":["import {\n DEADLINE_HEADER,\n deadlineFromHeader,\n DeadlineTag,\n type EntifixFailure,\n type FailureCategory,\n Internal,\n isFailure,\n PROBLEM_CONTENT_TYPE,\n problemOf,\n} from '@entifix/core';\nimport { Cause, Clock, Effect, Option } from 'effect';\nimport type { HttpBody } from 'effect/http';\nimport { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';\nimport type { Severity } from 'effect/LogLevel';\n\n/**\n * What a mounted handler may fail with: a categorised failure, or the\n * response body failing to encode. Anything else — a plain `Error`, a string —\n * does not compile at the mount, which is the point: a failure the shell cannot\n * categorise is one it cannot answer honestly\n * ([ADR 0013](../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).\n */\nexport type RouteFailure = EntifixFailure | HttpBody.HttpBodyError;\n\n/**\n * How a host answers and logs its failures: the status a category answers with\n * and the level it is logged at, each overridable per tag.\n *\n * The composition root owns it. A cell never decides a status, a retry or a\n * log line; it fails, and the shell's policy says what that means.\n */\nexport interface FailurePolicy {\n readonly categoryStatus: Readonly<Record<FailureCategory, number>>;\n readonly tagStatus: Readonly<Record<string, number>>;\n readonly categoryLogLevel: Readonly<Record<FailureCategory, Severity>>;\n readonly tagLogLevel: Readonly<Record<string, Severity>>;\n}\n\n/**\n * The defaults: the category table's statuses, business failures at `Debug`\n * because an answer is not an incident, the transient technical categories at\n * `Warn`, and what is broken — a peer speaking nonsense, a bug — at `Error`.\n */\nexport const defaultFailurePolicy: FailurePolicy = {\n categoryStatus: {\n invalid: 400,\n unauthenticated: 401,\n forbidden: 403,\n notFound: 404,\n conflict: 409,\n declined: 422,\n contention: 409,\n exhausted: 429,\n unavailable: 503,\n timeout: 504,\n protocol: 502,\n internal: 500,\n },\n tagStatus: {},\n categoryLogLevel: {\n invalid: 'Debug',\n unauthenticated: 'Debug',\n forbidden: 'Debug',\n notFound: 'Debug',\n conflict: 'Debug',\n declined: 'Debug',\n contention: 'Warn',\n exhausted: 'Warn',\n unavailable: 'Warn',\n timeout: 'Warn',\n protocol: 'Error',\n internal: 'Error',\n },\n tagLogLevel: {},\n};\n\nexport interface FailurePolicyOverrides {\n readonly categoryStatus?: Partial<Record<FailureCategory, number>>;\n readonly tagStatus?: Readonly<Record<string, number>>;\n readonly categoryLogLevel?: Partial<Record<FailureCategory, Severity>>;\n readonly tagLogLevel?: Readonly<Record<string, Severity>>;\n}\n\n/** A policy with some of its entries replaced — how a slice extends the fleet's. */\nexport const extendFailurePolicy = (\n base: FailurePolicy,\n overrides: FailurePolicyOverrides,\n): FailurePolicy => ({\n categoryStatus: { ...base.categoryStatus, ...overrides.categoryStatus },\n tagStatus: { ...base.tagStatus, ...overrides.tagStatus },\n categoryLogLevel: {\n ...base.categoryLogLevel,\n ...overrides.categoryLogLevel,\n },\n tagLogLevel: { ...base.tagLogLevel, ...overrides.tagLogLevel },\n});\n\nexport const statusFor = (\n failure: EntifixFailure,\n policy: FailurePolicy,\n): number =>\n policy.tagStatus[failure._tag] ?? policy.categoryStatus[failure.category];\n\nexport const logLevelFor = (\n failure: EntifixFailure,\n policy: FailurePolicy,\n): Severity =>\n policy.tagLogLevel[failure._tag] ?? policy.categoryLogLevel[failure.category];\n\nexport interface ProblemResponseOptions {\n readonly traceId?: string | undefined;\n readonly instance?: string | undefined;\n}\n\nconst retryAfterOf = (failure: EntifixFailure): number | undefined => {\n const seconds = (failure as { readonly retryAfterSeconds?: unknown })\n .retryAfterSeconds;\n return typeof seconds === 'number' ? seconds : undefined;\n};\n\n/**\n * The `application/problem+json` answer to a failure, with the policy's\n * status. Built synchronously — the body is already plain data — so it cannot\n * itself fail to encode.\n */\nexport const problemResponse = (\n failure: EntifixFailure,\n policy: FailurePolicy = defaultFailurePolicy,\n options: ProblemResponseOptions = {},\n): HttpServerResponse.HttpServerResponse => {\n const status = statusFor(failure, policy);\n const retryAfter = retryAfterOf(failure);\n return HttpServerResponse.text(\n JSON.stringify(\n problemOf(failure, {\n status,\n ...(options.traceId === undefined ? {} : { traceId: options.traceId }),\n ...(options.instance === undefined\n ? {}\n : { instance: options.instance }),\n }),\n ),\n {\n status,\n contentType: PROBLEM_CONTENT_TYPE,\n ...(retryAfter === undefined\n ? {}\n : { headers: { 'retry-after': String(retryAfter) } }),\n },\n );\n};\n\n/** The current span's trace id, when a span is open. */\nconst currentTraceId: Effect.Effect<string | undefined> = Effect.option(\n Effect.currentSpan,\n).pipe(\n Effect.map(span => Option.getOrUndefined(Option.map(span, s => s.traceId))),\n);\n\n/** The path being answered, when a request is in scope. */\nconst currentInstance: Effect.Effect<string | undefined> = Effect.serviceOption(\n HttpServerRequest.HttpServerRequest,\n).pipe(\n Effect.map(request =>\n Option.getOrUndefined(Option.map(request, r => r.url.split('?')[0])),\n ),\n);\n\nconst answerFailure = (failure: EntifixFailure, policy: FailurePolicy) =>\n Effect.gen(function* () {\n const traceId = yield* currentTraceId;\n const instance = yield* currentInstance;\n yield* Effect.logWithLevel(logLevelFor(failure, policy))(\n 'request failed',\n ).pipe(\n Effect.annotateLogs({\n code: failure.code,\n category: failure.category,\n tag: failure._tag,\n ...(traceId === undefined ? {} : { traceId }),\n }),\n );\n return problemResponse(failure, policy, { traceId, instance });\n });\n\n/**\n * The opaque `500` for anything nobody categorised. The defect is logged in\n * full here, where it was caught; the body carries the trace id to find that\n * line by, and nothing of the defect itself.\n */\nconst answerDefect = (defect: unknown) =>\n Effect.gen(function* () {\n const traceId = yield* currentTraceId;\n const instance = yield* currentInstance;\n yield* Effect.logError('request defect').pipe(\n Effect.annotateLogs({\n code: 'internal',\n category: 'internal',\n cause: Cause.pretty(Cause.die(defect)),\n ...(traceId === undefined ? {} : { traceId }),\n }),\n );\n return problemResponse(new Internal(), defaultFailurePolicy, {\n traceId,\n instance,\n });\n });\n\n/**\n * The request's deadline, when its caller sent one, provided to the handler\n * so a retry below it never starts past it and an outbound call forwards what\n * is left ([ADR 0014](../../../../docs/adr/0014-retry-once-at-the-layer-above-under-a-budget.md)).\n */\nconst withRequestDeadline = <A, E, R>(effect: Effect.Effect<A, E, R>) =>\n Effect.gen(function* () {\n const request = yield* Effect.serviceOption(\n HttpServerRequest.HttpServerRequest,\n );\n const now = yield* Clock.currentTimeMillis;\n const deadline = Option.isSome(request)\n ? deadlineFromHeader(request.value.headers[DEADLINE_HEADER], now)\n : undefined;\n return yield* deadline === undefined\n ? effect\n : Effect.provideService(effect, DeadlineTag, deadline);\n });\n\n/**\n * The terminal reaction: every failure answered as a problem body with the\n * policy's status and logged at the policy's level, every defect answered as\n * an opaque `500`. Interruption is left alone — a request whose client went\n * away has nobody to answer.\n */\nexport const respond = <E extends RouteFailure, R>(\n effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>,\n policy: FailurePolicy = defaultFailurePolicy,\n): Effect.Effect<HttpServerResponse.HttpServerResponse, never, R> =>\n withRequestDeadline(effect).pipe(\n Effect.catch((failure: E) =>\n isFailure(failure)\n ? answerFailure(failure, policy)\n : answerDefect(failure),\n ),\n Effect.catchDefect(answerDefect),\n );\n\n/**\n * Mounts a set of routes with every handler wrapped in {@link respond}, once,\n * at the composition root. A route whose handler fails with anything but a\n * {@link RouteFailure} does not compile here.\n */\nexport const mountRoutes = <\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const Routes extends ReadonlyArray<HttpRouter.Route<RouteFailure, any>>,\n>(\n routes: Routes,\n policy: FailurePolicy = defaultFailurePolicy,\n) =>\n HttpRouter.addAll(\n routes.map(route =>\n HttpRouter.route(\n route.method,\n route.path,\n respond(route.handler, policy),\n {\n uninterruptible: route.uninterruptible,\n },\n ),\n ) as unknown as {\n readonly [K in keyof Routes]: HttpRouter.Route<\n never,\n HttpRouter.Route.Context<Routes[K]>\n >;\n },\n );\n\n/**\n * The router's last net: a defect that escaped every route — one not mounted\n * through {@link mountRoutes}, or one thrown while a response was being\n * written — answers the same opaque `500` problem. It catches defects only,\n * never the whole cause, so an interruption still interrupts.\n */\nexport const problemNet = HttpRouter.middleware(\n effect => effect.pipe(Effect.catchDefect(answerDefect)),\n { global: true },\n);\n\n/**\n * What a mechanism reports instead of logging — a relay pass, a shutdown drain\n * ([ADR 0013](../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).\n * Structural, so the shell can log an outcome from a package it does not\n * depend on.\n */\nexport interface MechanismOutcome {\n readonly _tag: string;\n readonly failure?: unknown;\n readonly [field: string]: unknown;\n}\n\n/** The level each outcome is logged at; anything unnamed is `Debug`. */\nexport const MECHANISM_OUTCOME_LEVELS: Readonly<Record<string, Severity>> = {\n quarantined: 'Error',\n sweepFailed: 'Error',\n hookFailed: 'Error',\n drainFailed: 'Warn',\n draining: 'Info',\n drained: 'Debug',\n};\n\n/**\n * The fork site's logger for a mechanism's outcomes: the level from the\n * outcome's tag, the outcome's own fields as annotations, and a typed\n * failure's code and category beside them — never `String(error)`.\n */\nexport const logMechanismOutcome = (\n outcome: MechanismOutcome,\n): Effect.Effect<void> => {\n const { _tag, failure, ...fields } = outcome;\n const typed = isFailure(failure)\n ? { code: failure.code, category: failure.category, tag: failure._tag }\n : failure === undefined\n ? {}\n : { code: 'internal', category: 'internal' };\n return Effect.logWithLevel(MECHANISM_OUTCOME_LEVELS[_tag] ?? 'Debug')(\n _tag,\n ).pipe(Effect.annotateLogs({ ...fields, ...typed }));\n};\n"],"names":["DEADLINE_HEADER","deadlineFromHeader","DeadlineTag","Internal","isFailure","PROBLEM_CONTENT_TYPE","problemOf","Cause","Clock","Effect","Option","HttpRouter","HttpServerRequest","HttpServerResponse","defaultFailurePolicy","categoryStatus","invalid","unauthenticated","forbidden","notFound","conflict","declined","contention","exhausted","unavailable","timeout","protocol","internal","tagStatus","categoryLogLevel","tagLogLevel","extendFailurePolicy","base","overrides","statusFor","failure","policy","_tag","category","logLevelFor","retryAfterOf","seconds","retryAfterSeconds","undefined","problemResponse","options","status","retryAfter","text","JSON","stringify","traceId","instance","contentType","headers","String","currentTraceId","option","currentSpan","pipe","map","span","getOrUndefined","s","currentInstance","serviceOption","request","r","url","split","answerFailure","gen","logWithLevel","annotateLogs","code","tag","answerDefect","defect","logError","cause","pretty","die","withRequestDeadline","effect","now","currentTimeMillis","deadline","isSome","value","provideService","respond","catch","catchDefect","mountRoutes","routes","addAll","route","method","path","handler","uninterruptible","problemNet","middleware","global","MECHANISM_OUTCOME_LEVELS","quarantined","sweepFailed","hookFailed","drainFailed","draining","drained","logMechanismOutcome","outcome","fields","typed"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type EntifixFailure } from '@entifix/core';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { HttpServerResponse } from 'effect/http';
|
|
4
|
+
/**
|
|
5
|
+
* A save refused by a rule the entity's own deserializer does not make — a
|
|
6
|
+
* closed set, a rule over the other records — answered as `status` with
|
|
7
|
+
* `code`, which is what a client translates.
|
|
8
|
+
*/
|
|
9
|
+
export declare class EntityRouteRefusal {
|
|
10
|
+
readonly status: 400 | 409 | 422;
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly detail?: string | undefined;
|
|
13
|
+
readonly _tag = "EntityRouteRefusal";
|
|
14
|
+
constructor(status: 400 | 409 | 422, code: string, detail?: string | undefined);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The `500` for anything a route did not anticipate.
|
|
18
|
+
*
|
|
19
|
+
* @deprecated A route mounted through `mountRoutes` answers through `respond`,
|
|
20
|
+
* which never writes `String(error)`. Kept for a host's unmigrated routes and
|
|
21
|
+
* removed with the class tree.
|
|
22
|
+
*/
|
|
23
|
+
export declare const routeFailure: (error: unknown) => Effect.Effect<HttpServerResponse.HttpServerResponse, import("effect/http/HttpBody").HttpBodyError, never>;
|
|
24
|
+
/**
|
|
25
|
+
* @deprecated A route mounted through `mountRoutes` fails with typed failures
|
|
26
|
+
* and lets `respond` answer them. Kept for a host's unmigrated routes and
|
|
27
|
+
* removed with the class tree.
|
|
28
|
+
*
|
|
29
|
+
* The answer to a request the route refused: a `400` for what the client sent
|
|
30
|
+
* wrong, the refusal's own status for a rule a save broke, a business-family
|
|
31
|
+
* failure's category status, and a `500` for anything else.
|
|
32
|
+
*/
|
|
33
|
+
export declare const invalid: (error: unknown, label: string, code: string) => Effect.Effect<HttpServerResponse.HttpServerResponse, import("effect/http/HttpBody").HttpBodyError, never>;
|
|
34
|
+
/**
|
|
35
|
+
* What a framework route's errors mean, while some still arrive as the old
|
|
36
|
+
* class tree: a typed failure passes through to `respond`, a refusal answers
|
|
37
|
+
* as its own problem body, an unreachable store becomes `Unavailable`, and
|
|
38
|
+
* anything else is a defect — the opaque `500`, logged where `respond`
|
|
39
|
+
* catches it.
|
|
40
|
+
*
|
|
41
|
+
* @param fallbackCode the code a domain error carrying none answers with.
|
|
42
|
+
*/
|
|
43
|
+
export declare const legacyAnswer: (fallbackCode: string) => (error: unknown) => Effect.Effect<HttpServerResponse.HttpServerResponse, EntifixFailure>;
|
|
44
|
+
//# sourceMappingURL=route-responses.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-responses.d.ts","sourceRoot":"","sources":["../src/route-responses.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,cAAc,EAKpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;GAIG;AACH,qBAAa,kBAAkB;IAI3B,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;IAL1B,QAAQ,CAAC,IAAI,wBAAwB;gBAG1B,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,EACvB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,YAAA;CAE3B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,8GAIxC,CAAC;AASJ;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,OAAO,EAAE,OAAO,MAAM,EAAE,MAAM,MAAM,8GA8BtC,CAAC;AAmC9B;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GACtB,cAAc,MAAM,MAEnB,OAAO,OAAO,KACb,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,kBAAkB,EAAE,cAAc,CAezC,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { defaultStatus, EntifixBuildError, EntifixConnError, failureTypeUri, isFailure, PROBLEM_CONTENT_TYPE, Unavailable } from '@entifix/core';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
import { HttpServerResponse } from 'effect/http';
|
|
5
|
+
/**
|
|
6
|
+
* A save refused by a rule the entity's own deserializer does not make — a
|
|
7
|
+
* closed set, a rule over the other records — answered as `status` with
|
|
8
|
+
* `code`, which is what a client translates.
|
|
9
|
+
*/ export class EntityRouteRefusal {
|
|
10
|
+
constructor(status, code, detail){
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.detail = detail;
|
|
14
|
+
this._tag = 'EntityRouteRefusal';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The `500` for anything a route did not anticipate.
|
|
19
|
+
*
|
|
20
|
+
* @deprecated A route mounted through `mountRoutes` answers through `respond`,
|
|
21
|
+
* which never writes `String(error)`. Kept for a host's unmigrated routes and
|
|
22
|
+
* removed with the class tree.
|
|
23
|
+
*/ export const routeFailure = (error)=>HttpServerResponse.json({
|
|
24
|
+
error: 'request failed',
|
|
25
|
+
code: 'unexpected',
|
|
26
|
+
detail: String(error)
|
|
27
|
+
}, {
|
|
28
|
+
status: 500
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* A domain error may carry its own code (`mediaNotHttps`), so the client can
|
|
32
|
+
* say which rule failed rather than "invalid body".
|
|
33
|
+
*/ const refusalCode = (error)=>'code' in error && typeof error.code === 'string' ? error.code : undefined;
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated A route mounted through `mountRoutes` fails with typed failures
|
|
36
|
+
* and lets `respond` answer them. Kept for a host's unmigrated routes and
|
|
37
|
+
* removed with the class tree.
|
|
38
|
+
*
|
|
39
|
+
* The answer to a request the route refused: a `400` for what the client sent
|
|
40
|
+
* wrong, the refusal's own status for a rule a save broke, a business-family
|
|
41
|
+
* failure's category status, and a `500` for anything else.
|
|
42
|
+
*/ export const invalid = (error, label, code)=>{
|
|
43
|
+
var _refusalCode;
|
|
44
|
+
return isFailure(error) && error.family === 'business' ? HttpServerResponse.json(_extends({
|
|
45
|
+
error: label,
|
|
46
|
+
// A body that is not an envelope is the route's own refusal, named
|
|
47
|
+
// by the route as it always was; any other failure names itself.
|
|
48
|
+
code: error._tag === 'InvalidEnvelope' ? code : error.code
|
|
49
|
+
}, error.message === '' ? {} : {
|
|
50
|
+
detail: error.message
|
|
51
|
+
}), {
|
|
52
|
+
status: defaultStatus(error.category)
|
|
53
|
+
}) : error instanceof EntifixBuildError ? HttpServerResponse.json({
|
|
54
|
+
error: label,
|
|
55
|
+
code: (_refusalCode = refusalCode(error)) != null ? _refusalCode : code,
|
|
56
|
+
detail: error.message
|
|
57
|
+
}, {
|
|
58
|
+
status: 400
|
|
59
|
+
}) : error instanceof EntityRouteRefusal ? HttpServerResponse.json(_extends({
|
|
60
|
+
error: 'the record cannot be saved',
|
|
61
|
+
code: error.code
|
|
62
|
+
}, error.detail === undefined ? {} : {
|
|
63
|
+
detail: error.detail
|
|
64
|
+
}), {
|
|
65
|
+
status: error.status
|
|
66
|
+
}) : routeFailure(error);
|
|
67
|
+
};
|
|
68
|
+
const LEGACY_CATEGORY = {
|
|
69
|
+
400: 'invalid',
|
|
70
|
+
409: 'conflict',
|
|
71
|
+
422: 'declined'
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* A problem body for a refusal whose code no failure class registered — a
|
|
75
|
+
* host's `EntityRouteRefusal`, a domain `EntifixBuildError` carrying its own
|
|
76
|
+
* code. Written by hand because there is no class to encode it through; a
|
|
77
|
+
* client that decodes it sees an unknown code until the refusal becomes a
|
|
78
|
+
* failure of its own.
|
|
79
|
+
*/ const legacyProblem = (status, code, detail)=>HttpServerResponse.text(JSON.stringify(_extends({
|
|
80
|
+
type: failureTypeUri(code),
|
|
81
|
+
title: code,
|
|
82
|
+
status,
|
|
83
|
+
code,
|
|
84
|
+
category: LEGACY_CATEGORY[status]
|
|
85
|
+
}, detail === undefined ? {} : {
|
|
86
|
+
detail
|
|
87
|
+
})), {
|
|
88
|
+
status,
|
|
89
|
+
contentType: PROBLEM_CONTENT_TYPE
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* What a framework route's errors mean, while some still arrive as the old
|
|
93
|
+
* class tree: a typed failure passes through to `respond`, a refusal answers
|
|
94
|
+
* as its own problem body, an unreachable store becomes `Unavailable`, and
|
|
95
|
+
* anything else is a defect — the opaque `500`, logged where `respond`
|
|
96
|
+
* catches it.
|
|
97
|
+
*
|
|
98
|
+
* @param fallbackCode the code a domain error carrying none answers with.
|
|
99
|
+
*/ export const legacyAnswer = (fallbackCode)=>(error)=>{
|
|
100
|
+
var _refusalCode;
|
|
101
|
+
return isFailure(error) ? Effect.fail(error) : error instanceof EntityRouteRefusal ? Effect.succeed(legacyProblem(error.status, error.code, error.detail)) : error instanceof EntifixBuildError ? Effect.succeed(legacyProblem(400, (_refusalCode = refusalCode(error)) != null ? _refusalCode : fallbackCode, error.message)) : error instanceof EntifixConnError ? Effect.fail(new Unavailable({})) : Effect.die(error);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
//# sourceMappingURL=route-responses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/route-responses.ts"],"sourcesContent":["import {\n defaultStatus,\n EntifixBuildError,\n EntifixConnError,\n type EntifixFailure,\n failureTypeUri,\n isFailure,\n PROBLEM_CONTENT_TYPE,\n Unavailable,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport { HttpServerResponse } from 'effect/http';\n\n/**\n * A save refused by a rule the entity's own deserializer does not make — a\n * closed set, a rule over the other records — answered as `status` with\n * `code`, which is what a client translates.\n */\nexport class EntityRouteRefusal {\n readonly _tag = 'EntityRouteRefusal';\n\n constructor(\n readonly status: 400 | 409 | 422,\n readonly code: string,\n readonly detail?: string,\n ) {}\n}\n\n/**\n * The `500` for anything a route did not anticipate.\n *\n * @deprecated A route mounted through `mountRoutes` answers through `respond`,\n * which never writes `String(error)`. Kept for a host's unmigrated routes and\n * removed with the class tree.\n */\nexport const routeFailure = (error: unknown) =>\n HttpServerResponse.json(\n { error: 'request failed', code: 'unexpected', detail: String(error) },\n { status: 500 },\n );\n\n/**\n * A domain error may carry its own code (`mediaNotHttps`), so the client can\n * say which rule failed rather than \"invalid body\".\n */\nconst refusalCode = (error: EntifixBuildError): string | undefined =>\n 'code' in error && typeof error.code === 'string' ? error.code : undefined;\n\n/**\n * @deprecated A route mounted through `mountRoutes` fails with typed failures\n * and lets `respond` answer them. Kept for a host's unmigrated routes and\n * removed with the class tree.\n *\n * The answer to a request the route refused: a `400` for what the client sent\n * wrong, the refusal's own status for a rule a save broke, a business-family\n * failure's category status, and a `500` for anything else.\n */\nexport const invalid = (error: unknown, label: string, code: string) =>\n isFailure(error) && error.family === 'business'\n ? HttpServerResponse.json(\n {\n error: label,\n // A body that is not an envelope is the route's own refusal, named\n // by the route as it always was; any other failure names itself.\n code: error._tag === 'InvalidEnvelope' ? code : error.code,\n ...(error.message === '' ? {} : { detail: error.message }),\n },\n { status: defaultStatus(error.category) },\n )\n : error instanceof EntifixBuildError\n ? HttpServerResponse.json(\n {\n error: label,\n code: refusalCode(error) ?? code,\n detail: error.message,\n },\n { status: 400 },\n )\n : error instanceof EntityRouteRefusal\n ? HttpServerResponse.json(\n {\n error: 'the record cannot be saved',\n code: error.code,\n ...(error.detail === undefined ? {} : { detail: error.detail }),\n },\n { status: error.status },\n )\n : routeFailure(error);\n\nconst LEGACY_CATEGORY: Record<\n EntityRouteRefusal['status'],\n 'invalid' | 'conflict' | 'declined'\n> = {\n 400: 'invalid',\n 409: 'conflict',\n 422: 'declined',\n};\n\n/**\n * A problem body for a refusal whose code no failure class registered — a\n * host's `EntityRouteRefusal`, a domain `EntifixBuildError` carrying its own\n * code. Written by hand because there is no class to encode it through; a\n * client that decodes it sees an unknown code until the refusal becomes a\n * failure of its own.\n */\nconst legacyProblem = (\n status: EntityRouteRefusal['status'],\n code: string,\n detail?: string,\n) =>\n HttpServerResponse.text(\n JSON.stringify({\n type: failureTypeUri(code),\n title: code,\n status,\n code,\n category: LEGACY_CATEGORY[status],\n ...(detail === undefined ? {} : { detail }),\n }),\n { status, contentType: PROBLEM_CONTENT_TYPE },\n );\n\n/**\n * What a framework route's errors mean, while some still arrive as the old\n * class tree: a typed failure passes through to `respond`, a refusal answers\n * as its own problem body, an unreachable store becomes `Unavailable`, and\n * anything else is a defect — the opaque `500`, logged where `respond`\n * catches it.\n *\n * @param fallbackCode the code a domain error carrying none answers with.\n */\nexport const legacyAnswer =\n (fallbackCode: string) =>\n (\n error: unknown,\n ): Effect.Effect<HttpServerResponse.HttpServerResponse, EntifixFailure> =>\n isFailure(error)\n ? Effect.fail(error)\n : error instanceof EntityRouteRefusal\n ? Effect.succeed(legacyProblem(error.status, error.code, error.detail))\n : error instanceof EntifixBuildError\n ? Effect.succeed(\n legacyProblem(\n 400,\n refusalCode(error) ?? fallbackCode,\n error.message,\n ),\n )\n : error instanceof EntifixConnError\n ? Effect.fail(new Unavailable({}))\n : Effect.die(error);\n"],"names":["defaultStatus","EntifixBuildError","EntifixConnError","failureTypeUri","isFailure","PROBLEM_CONTENT_TYPE","Unavailable","Effect","HttpServerResponse","EntityRouteRefusal","status","code","detail","_tag","routeFailure","error","json","String","refusalCode","undefined","invalid","label","family","message","category","LEGACY_CATEGORY","legacyProblem","text","JSON","stringify","type","title","contentType","legacyAnswer","fallbackCode","fail","succeed","die"],"mappings":";AAAA,SACEA,aAAa,EACbC,iBAAiB,EACjBC,gBAAgB,EAEhBC,cAAc,EACdC,SAAS,EACTC,oBAAoB,EACpBC,WAAW,QACN,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAChC,SAASC,kBAAkB,QAAQ,cAAc;AAEjD;;;;CAIC,GACD,OAAO,MAAMC;IAGX,YACE,AAASC,MAAuB,EAChC,AAASC,IAAY,EACrB,AAASC,MAAe,CACxB;aAHSF,SAAAA;aACAC,OAAAA;aACAC,SAAAA;aALFC,OAAO;IAMb;AACL;AAEA;;;;;;CAMC,GACD,OAAO,MAAMC,eAAe,CAACC,QAC3BP,mBAAmBQ,IAAI,CACrB;QAAED,OAAO;QAAkBJ,MAAM;QAAcC,QAAQK,OAAOF;IAAO,GACrE;QAAEL,QAAQ;IAAI,GACd;AAEJ;;;CAGC,GACD,MAAMQ,cAAc,CAACH,QACnB,UAAUA,SAAS,OAAOA,MAAMJ,IAAI,KAAK,WAAWI,MAAMJ,IAAI,GAAGQ;AAEnE;;;;;;;;CAQC,GACD,OAAO,MAAMC,UAAU,CAACL,OAAgBM,OAAeV;QAgBrCO;WAfhBd,UAAUW,UAAUA,MAAMO,MAAM,KAAK,aACjCd,mBAAmBQ,IAAI,CACrB;QACED,OAAOM;QACP,mEAAmE;QACnE,iEAAiE;QACjEV,MAAMI,MAAMF,IAAI,KAAK,oBAAoBF,OAAOI,MAAMJ,IAAI;OACtDI,MAAMQ,OAAO,KAAK,KAAK,CAAC,IAAI;QAAEX,QAAQG,MAAMQ,OAAO;IAAC,IAE1D;QAAEb,QAAQV,cAAce,MAAMS,QAAQ;IAAE,KAE1CT,iBAAiBd,oBACfO,mBAAmBQ,IAAI,CACrB;QACED,OAAOM;QACPV,IAAI,GAAEO,eAAAA,YAAYH,kBAAZG,eAAsBP;QAC5BC,QAAQG,MAAMQ,OAAO;IACvB,GACA;QAAEb,QAAQ;IAAI,KAEhBK,iBAAiBN,qBACfD,mBAAmBQ,IAAI,CACrB;QACED,OAAO;QACPJ,MAAMI,MAAMJ,IAAI;OACZI,MAAMH,MAAM,KAAKO,YAAY,CAAC,IAAI;QAAEP,QAAQG,MAAMH,MAAM;IAAC,IAE/D;QAAEF,QAAQK,MAAML,MAAM;IAAC,KAEzBI,aAAaC;EAAO;AAE9B,MAAMU,kBAGF;IACF,KAAK;IACL,KAAK;IACL,KAAK;AACP;AAEA;;;;;;CAMC,GACD,MAAMC,gBAAgB,CACpBhB,QACAC,MACAC,SAEAJ,mBAAmBmB,IAAI,CACrBC,KAAKC,SAAS,CAAC;QACbC,MAAM3B,eAAeQ;QACrBoB,OAAOpB;QACPD;QACAC;QACAa,UAAUC,eAAe,CAACf,OAAO;OAC7BE,WAAWO,YAAY,CAAC,IAAI;QAAEP;IAAO,KAE3C;QAAEF;QAAQsB,aAAa3B;IAAqB;AAGhD;;;;;;;;CAQC,GACD,OAAO,MAAM4B,eACX,CAACC,eACD,CACEnB;YAUYG;eARZd,UAAUW,SACNR,OAAO4B,IAAI,CAACpB,SACZA,iBAAiBN,qBACfF,OAAO6B,OAAO,CAACV,cAAcX,MAAML,MAAM,EAAEK,MAAMJ,IAAI,EAAEI,MAAMH,MAAM,KACnEG,iBAAiBd,oBACfM,OAAO6B,OAAO,CACZV,cACE,MACAR,eAAAA,YAAYH,kBAAZG,eAAsBgB,cACtBnB,MAAMQ,OAAO,KAGjBR,iBAAiBb,mBACfK,OAAO4B,IAAI,CAAC,IAAI7B,YAAY,CAAC,MAC7BC,OAAO8B,GAAG,CAACtB;MAAO"}
|
|
@@ -62,5 +62,5 @@ export interface ServiceDescription {
|
|
|
62
62
|
* The names come from a probe's `targets`, which are Store register names by
|
|
63
63
|
* construction.
|
|
64
64
|
*/
|
|
65
|
-
export declare const serviceDescriptionRoute: (serviceName: string, slices: readonly string[], guard: SharedToken) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", HealthRegistryTag> | HttpRouter.Request<"
|
|
65
|
+
export declare const serviceDescriptionRoute: (serviceName: string, slices: readonly string[], guard: SharedToken) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", HealthRegistryTag> | HttpRouter.Request<"Requires", WiringRegistryTag>>;
|
|
66
66
|
//# sourceMappingURL=service-description-route.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-description-route.d.ts","sourceRoot":"","sources":["../src/service-description-route.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAa,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEzE,OAAO,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"service-description-route.d.ts","sourceRoot":"","sources":["../src/service-description-route.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAa,MAAM,mBAAmB,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEzE,OAAO,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG9E;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AAExD,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC9B,yEAAyE;IACzE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;IAC5C,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,QAAQ,CAAC,SAAS,EAAE,SAAS,eAAe,EAAE,CAAC;IAC/C,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,mDAAmD;IACnD,QAAQ,CAAC,aAAa,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACtD;AAwCD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,uBAAuB,GAClC,aAAa,MAAM,EACnB,QAAQ,SAAS,MAAM,EAAE,EACzB,OAAO,WAAW,8KAYjB,CAAC"}
|
|
@@ -2,6 +2,7 @@ import { HealthRegistryTag, WiringRegistryTag } from '@entifix/business';
|
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
3
|
import { HttpRouter, HttpServerResponse } from 'effect/http';
|
|
4
4
|
import { requireSharedToken } from './auth/shared-token.js';
|
|
5
|
+
import { respond } from './respond.js';
|
|
5
6
|
/**
|
|
6
7
|
* The path the wiring document is served at.
|
|
7
8
|
*
|
|
@@ -54,6 +55,6 @@ import { requireSharedToken } from './auth/shared-token.js';
|
|
|
54
55
|
* organizations, so leaking one turns a wiring document into a customer list.
|
|
55
56
|
* The names come from a probe's `targets`, which are Store register names by
|
|
56
57
|
* construction.
|
|
57
|
-
*/ export const serviceDescriptionRoute = (serviceName, slices, guard)=>HttpRouter.add('GET', SERVICE_DESCRIPTION_PATH, requireSharedToken(guard)(Effect.flatMap(describe(serviceName, slices), (description)=>HttpServerResponse.json(description))));
|
|
58
|
+
*/ export const serviceDescriptionRoute = (serviceName, slices, guard)=>HttpRouter.add('GET', SERVICE_DESCRIPTION_PATH, respond(requireSharedToken(guard)(Effect.flatMap(describe(serviceName, slices), (description)=>HttpServerResponse.json(description)))));
|
|
58
59
|
|
|
59
60
|
//# sourceMappingURL=service-description-route.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/service-description-route.ts"],"sourcesContent":["import type { BoundSubscription, ProbeKind } from '@entifix/business';\nimport { HealthRegistryTag, WiringRegistryTag } from '@entifix/business';\nimport { Effect } from 'effect';\nimport { HttpRouter, HttpServerResponse } from 'effect/http';\n\nimport { requireSharedToken, type SharedToken } from './auth/shared-token.js';\n\n/**\n * The path the wiring document is served at.\n *\n * Two **static** segments, which matters: `HttpRouter` resolves through\n * `find-my-way-ts`, where a static segment beats a parametric one but there is\n * no backtracking once a parametric branch has matched. A `/api/:something`\n * route registered anywhere in the same router would otherwise swallow this one\n * and answer its own `404` — the failure the `$metadata` route hit first.\n */\nexport const SERVICE_DESCRIPTION_PATH = '/api/$service';\n\n/** One thing this process is connected to, named logically. */\nexport interface DescribedTarget {\n /** The Store's register name, an exchange, or a service. Never a URI. */\n readonly name: string;\n /** The readiness probe that covers it, so the two documents line up. */\n readonly probe: string;\n}\n\n/**\n * What a service says about its own wiring.\n *\n * Shape, not values — `/api/config` is the sibling that serves the parameters a\n * service was given, redacted. Keeping them apart is deliberate: one is redacted\n * because it holds credentials, the other gated because it holds a map, and\n * merging them forces the stricter rule onto both.\n */\nexport interface ServiceDescription {\n readonly service: string;\n /**\n * The slices this process hosts. More than one means co-deployment\n * — `marketplace-admin-service` runs the `transaction` slice too.\n */\n readonly slices: readonly string[];\n readonly stores: readonly DescribedTarget[];\n readonly brokers: readonly DescribedTarget[];\n readonly upstreams: readonly DescribedTarget[];\n /** Distinct event names this process has emitted since boot. */\n readonly published: readonly string[];\n /** The bus bindings this process actually made. */\n readonly subscriptions: readonly BoundSubscription[];\n}\n\n/**\n * Everything the two registries know, sorted by what it is.\n *\n * Generated from the **health probe registrations**, which is the one source\n * that cannot drift: a service that gains a datastore gains its probe by merging\n * the client's probe layer, so it gains a line here with no edit of its own.\n */\nconst describe = (\n service: string,\n slices: readonly string[],\n): Effect.Effect<\n ServiceDescription,\n never,\n HealthRegistryTag | WiringRegistryTag\n> =>\n Effect.gen(function* () {\n const health = yield* HealthRegistryTag;\n const wiring = yield* WiringRegistryTag;\n const probes = yield* health.probes;\n\n const ofKind = (kind: ProbeKind): readonly DescribedTarget[] =>\n probes\n .filter(probe => probe.kind === kind)\n .flatMap(probe =>\n probe.targets.map(name => ({ name, probe: probe.name })),\n );\n\n return {\n service,\n slices,\n stores: ofKind('datastore'),\n brokers: ofKind('broker'),\n upstreams: ofKind('upstream'),\n published: yield* wiring.published,\n subscriptions: yield* wiring.subscriptions,\n };\n });\n\n/**\n * `GET /api/$service`, as a route layer to merge beside a service's own.\n *\n * **Gated by a shared token**, which the host names. This names every store, exchange and upstream\n * a service has, which is a reconnaissance map rather than diagnostics. The\n * repository's precedent on maps of the model is uniform: `$metadata` answers\n * `404` rather than `403` so it cannot be walked as an oracle, readiness serves\n * probe names only, and `redactConfiguration` blanks anything flagged secret\n * because `/api/config` is unauthenticated. The health endpoints stay open —\n * a probe cannot hold a credential.\n *\n * The token is fleet membership, which is the right shape here: the readers are\n * tooling and CI, not a person and not a browser.\n *\n * **Logical names only.** Nothing in the response is a URI, and emphatically\n * nothing is `tenant_<organizationId>` — the tenant databases are named after\n * organizations, so leaking one turns a wiring document into a customer list.\n * The names come from a probe's `targets`, which are Store register names by\n * construction.\n */\nexport const serviceDescriptionRoute = (\n serviceName: string,\n slices: readonly string[],\n guard: SharedToken,\n) =>\n HttpRouter.add(\n 'GET',\n SERVICE_DESCRIPTION_PATH,\n requireSharedToken(guard)(\n
|
|
1
|
+
{"version":3,"sources":["../src/service-description-route.ts"],"sourcesContent":["import type { BoundSubscription, ProbeKind } from '@entifix/business';\nimport { HealthRegistryTag, WiringRegistryTag } from '@entifix/business';\nimport { Effect } from 'effect';\nimport { HttpRouter, HttpServerResponse } from 'effect/http';\n\nimport { requireSharedToken, type SharedToken } from './auth/shared-token.js';\nimport { respond } from './respond.js';\n\n/**\n * The path the wiring document is served at.\n *\n * Two **static** segments, which matters: `HttpRouter` resolves through\n * `find-my-way-ts`, where a static segment beats a parametric one but there is\n * no backtracking once a parametric branch has matched. A `/api/:something`\n * route registered anywhere in the same router would otherwise swallow this one\n * and answer its own `404` — the failure the `$metadata` route hit first.\n */\nexport const SERVICE_DESCRIPTION_PATH = '/api/$service';\n\n/** One thing this process is connected to, named logically. */\nexport interface DescribedTarget {\n /** The Store's register name, an exchange, or a service. Never a URI. */\n readonly name: string;\n /** The readiness probe that covers it, so the two documents line up. */\n readonly probe: string;\n}\n\n/**\n * What a service says about its own wiring.\n *\n * Shape, not values — `/api/config` is the sibling that serves the parameters a\n * service was given, redacted. Keeping them apart is deliberate: one is redacted\n * because it holds credentials, the other gated because it holds a map, and\n * merging them forces the stricter rule onto both.\n */\nexport interface ServiceDescription {\n readonly service: string;\n /**\n * The slices this process hosts. More than one means co-deployment\n * — `marketplace-admin-service` runs the `transaction` slice too.\n */\n readonly slices: readonly string[];\n readonly stores: readonly DescribedTarget[];\n readonly brokers: readonly DescribedTarget[];\n readonly upstreams: readonly DescribedTarget[];\n /** Distinct event names this process has emitted since boot. */\n readonly published: readonly string[];\n /** The bus bindings this process actually made. */\n readonly subscriptions: readonly BoundSubscription[];\n}\n\n/**\n * Everything the two registries know, sorted by what it is.\n *\n * Generated from the **health probe registrations**, which is the one source\n * that cannot drift: a service that gains a datastore gains its probe by merging\n * the client's probe layer, so it gains a line here with no edit of its own.\n */\nconst describe = (\n service: string,\n slices: readonly string[],\n): Effect.Effect<\n ServiceDescription,\n never,\n HealthRegistryTag | WiringRegistryTag\n> =>\n Effect.gen(function* () {\n const health = yield* HealthRegistryTag;\n const wiring = yield* WiringRegistryTag;\n const probes = yield* health.probes;\n\n const ofKind = (kind: ProbeKind): readonly DescribedTarget[] =>\n probes\n .filter(probe => probe.kind === kind)\n .flatMap(probe =>\n probe.targets.map(name => ({ name, probe: probe.name })),\n );\n\n return {\n service,\n slices,\n stores: ofKind('datastore'),\n brokers: ofKind('broker'),\n upstreams: ofKind('upstream'),\n published: yield* wiring.published,\n subscriptions: yield* wiring.subscriptions,\n };\n });\n\n/**\n * `GET /api/$service`, as a route layer to merge beside a service's own.\n *\n * **Gated by a shared token**, which the host names. This names every store, exchange and upstream\n * a service has, which is a reconnaissance map rather than diagnostics. The\n * repository's precedent on maps of the model is uniform: `$metadata` answers\n * `404` rather than `403` so it cannot be walked as an oracle, readiness serves\n * probe names only, and `redactConfiguration` blanks anything flagged secret\n * because `/api/config` is unauthenticated. The health endpoints stay open —\n * a probe cannot hold a credential.\n *\n * The token is fleet membership, which is the right shape here: the readers are\n * tooling and CI, not a person and not a browser.\n *\n * **Logical names only.** Nothing in the response is a URI, and emphatically\n * nothing is `tenant_<organizationId>` — the tenant databases are named after\n * organizations, so leaking one turns a wiring document into a customer list.\n * The names come from a probe's `targets`, which are Store register names by\n * construction.\n */\nexport const serviceDescriptionRoute = (\n serviceName: string,\n slices: readonly string[],\n guard: SharedToken,\n) =>\n HttpRouter.add(\n 'GET',\n SERVICE_DESCRIPTION_PATH,\n respond(\n requireSharedToken(guard)(\n Effect.flatMap(describe(serviceName, slices), description =>\n HttpServerResponse.json(description),\n ),\n ),\n ),\n );\n"],"names":["HealthRegistryTag","WiringRegistryTag","Effect","HttpRouter","HttpServerResponse","requireSharedToken","respond","SERVICE_DESCRIPTION_PATH","describe","service","slices","gen","health","wiring","probes","ofKind","kind","filter","probe","flatMap","targets","map","name","stores","brokers","upstreams","published","subscriptions","serviceDescriptionRoute","serviceName","guard","add","description","json"],"mappings":"AACA,SAASA,iBAAiB,EAAEC,iBAAiB,QAAQ,oBAAoB;AACzE,SAASC,MAAM,QAAQ,SAAS;AAChC,SAASC,UAAU,EAAEC,kBAAkB,QAAQ,cAAc;AAE7D,SAASC,kBAAkB,QAA0B,yBAAyB;AAC9E,SAASC,OAAO,QAAQ,eAAe;AAEvC;;;;;;;;CAQC,GACD,OAAO,MAAMC,2BAA2B,gBAAgB;AAkCxD;;;;;;CAMC,GACD,MAAMC,WAAW,CACfC,SACAC,SAMAR,OAAOS,GAAG,CAAC;QACT,MAAMC,SAAS,OAAOZ;QACtB,MAAMa,SAAS,OAAOZ;QACtB,MAAMa,SAAS,OAAOF,OAAOE,MAAM;QAEnC,MAAMC,SAAS,CAACC,OACdF,OACGG,MAAM,CAACC,CAAAA,QAASA,MAAMF,IAAI,KAAKA,MAC/BG,OAAO,CAACD,CAAAA,QACPA,MAAME,OAAO,CAACC,GAAG,CAACC,CAAAA,OAAS,CAAA;wBAAEA;wBAAMJ,OAAOA,MAAMI,IAAI;oBAAC,CAAA;QAG3D,OAAO;YACLb;YACAC;YACAa,QAAQR,OAAO;YACfS,SAAST,OAAO;YAChBU,WAAWV,OAAO;YAClBW,WAAW,OAAOb,OAAOa,SAAS;YAClCC,eAAe,OAAOd,OAAOc,aAAa;QAC5C;IACF;AAEF;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,MAAMC,0BAA0B,CACrCC,aACAnB,QACAoB,QAEA3B,WAAW4B,GAAG,CACZ,OACAxB,0BACAD,QACED,mBAAmByB,OACjB5B,OAAOiB,OAAO,CAACX,SAASqB,aAAanB,SAASsB,CAAAA,cAC5C5B,mBAAmB6B,IAAI,CAACD,iBAI9B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entifix/service-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Effect HTTP service skeleton: server layer, health and metadata routes, permission guards, remote configuration and OpenTelemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -66,33 +66,33 @@
|
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@swc/helpers": "0.5.23",
|
|
69
|
-
"@entifix/
|
|
70
|
-
"@entifix/
|
|
71
|
-
"@entifix/core": "0.
|
|
72
|
-
"@entifix/
|
|
69
|
+
"@entifix/authz": "0.10.0",
|
|
70
|
+
"@entifix/business": "0.10.0",
|
|
71
|
+
"@entifix/core": "0.10.0",
|
|
72
|
+
"@entifix/tooling": "0.10.0"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@effect/opentelemetry": "4.0.
|
|
76
|
-
"@effect/platform-node": "4.0.
|
|
75
|
+
"@effect/opentelemetry": "4.0.6",
|
|
76
|
+
"@effect/platform-node": "4.0.6",
|
|
77
77
|
"@opentelemetry/api": "1.9.1",
|
|
78
78
|
"@opentelemetry/context-async-hooks": "2.12.0",
|
|
79
79
|
"@opentelemetry/exporter-metrics-otlp-http": "0.223.0",
|
|
80
80
|
"@opentelemetry/exporter-trace-otlp-http": "0.223.0",
|
|
81
81
|
"@opentelemetry/sdk-metrics": "2.12.0",
|
|
82
82
|
"@opentelemetry/sdk-trace-base": "2.12.0",
|
|
83
|
-
"effect": "4.0.
|
|
84
|
-
"@entifix/testing-unit": "0.
|
|
83
|
+
"effect": "4.0.6",
|
|
84
|
+
"@entifix/testing-unit": "0.10.0"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
|
-
"@effect/opentelemetry": "^4.0.
|
|
88
|
-
"@effect/platform-node": "^4.0.
|
|
87
|
+
"@effect/opentelemetry": "^4.0.6",
|
|
88
|
+
"@effect/platform-node": "^4.0.6",
|
|
89
89
|
"@opentelemetry/api": ">=1.9 <2",
|
|
90
90
|
"@opentelemetry/context-async-hooks": ">=2.12 <3",
|
|
91
91
|
"@opentelemetry/exporter-metrics-otlp-http": ">=0.223 <0.224",
|
|
92
92
|
"@opentelemetry/exporter-trace-otlp-http": ">=0.223 <0.224",
|
|
93
93
|
"@opentelemetry/sdk-metrics": ">=2.12 <3",
|
|
94
94
|
"@opentelemetry/sdk-trace-base": ">=2.12 <3",
|
|
95
|
-
"effect": "^4.0.
|
|
95
|
+
"effect": "^4.0.6"
|
|
96
96
|
},
|
|
97
97
|
"peerDependenciesMeta": {
|
|
98
98
|
"@effect/opentelemetry": {
|