@entifix/service-shell 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/auth/failures.d.ts +23 -0
  2. package/dist/auth/failures.d.ts.map +1 -0
  3. package/dist/auth/failures.js +20 -0
  4. package/dist/auth/failures.js.map +1 -0
  5. package/dist/auth/index.d.ts +1 -0
  6. package/dist/auth/index.d.ts.map +1 -1
  7. package/dist/auth/index.js +1 -0
  8. package/dist/auth/index.js.map +1 -1
  9. package/dist/auth/require-organization.d.ts +1 -2
  10. package/dist/auth/require-organization.d.ts.map +1 -1
  11. package/dist/auth/require-organization.js +2 -7
  12. package/dist/auth/require-organization.js.map +1 -1
  13. package/dist/auth/require-principal.d.ts +3 -3
  14. package/dist/auth/require-principal.d.ts.map +1 -1
  15. package/dist/auth/require-principal.js +6 -14
  16. package/dist/auth/require-principal.js.map +1 -1
  17. package/dist/auth/require-service-crossing.d.ts +3 -3
  18. package/dist/auth/require-service-crossing.d.ts.map +1 -1
  19. package/dist/auth/require-service-crossing.js +18 -21
  20. package/dist/auth/require-service-crossing.js.map +1 -1
  21. package/dist/auth/shared-token.d.ts +2 -2
  22. package/dist/auth/shared-token.d.ts.map +1 -1
  23. package/dist/auth/shared-token.js +7 -8
  24. package/dist/auth/shared-token.js.map +1 -1
  25. package/dist/entity-facet-routes.d.ts +4 -1
  26. package/dist/entity-facet-routes.d.ts.map +1 -1
  27. package/dist/entity-facet-routes.js +5 -4
  28. package/dist/entity-facet-routes.js.map +1 -1
  29. package/dist/entity-metadata-route.d.ts +1 -1
  30. package/dist/entity-metadata-route.d.ts.map +1 -1
  31. package/dist/entity-metadata-route.js +6 -9
  32. package/dist/entity-metadata-route.js.map +1 -1
  33. package/dist/entity-route-guard.d.ts +1 -1
  34. package/dist/entity-route-guard.d.ts.map +1 -1
  35. package/dist/entity-routes.d.ts +10 -4
  36. package/dist/entity-routes.d.ts.map +1 -1
  37. package/dist/entity-routes.js +14 -14
  38. package/dist/entity-routes.js.map +1 -1
  39. package/dist/health-routes.d.ts.map +1 -1
  40. package/dist/health-routes.js +3 -2
  41. package/dist/health-routes.js.map +1 -1
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/make-service.d.ts.map +1 -1
  47. package/dist/make-service.js +4 -2
  48. package/dist/make-service.js.map +1 -1
  49. package/dist/respond.d.ts +92 -0
  50. package/dist/respond.d.ts.map +1 -0
  51. package/dist/respond.js +178 -0
  52. package/dist/respond.js.map +1 -0
  53. package/dist/route-responses.d.ts +27 -5
  54. package/dist/route-responses.d.ts.map +1 -1
  55. package/dist/route-responses.js +60 -5
  56. package/dist/route-responses.js.map +1 -1
  57. package/dist/service-description-route.d.ts +1 -1
  58. package/dist/service-description-route.d.ts.map +1 -1
  59. package/dist/service-description-route.js +2 -1
  60. package/dist/service-description-route.js.map +1 -1
  61. package/package.json +12 -12
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/entity-facet-routes.ts"],"sourcesContent":["import {\n EntityFacetCounterFactoryTag,\n EntityFacetCounterTag,\n facetCountsUCFactory,\n} from '@entifix/business';\nimport {\n EntifixBuildError,\n type Entity,\n type EntityConstructor,\n type EntityFacetCounts,\n type EntityFacetQuery,\n envelopeEntityName,\n makeEntityFacetsEnvelope,\n parseFacetQueryParams,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';\n\nimport {\n type EntityReadScope,\n type EntityRouteContext,\n type EntityRouteGuard,\n guardRoute,\n type GuardServices,\n} from './entity-route-guard.js';\nimport { invalid } from './route-responses.js';\n\nexport interface EntityFacetRoutesOptions<\n T extends Entity,\n R,\n G extends EntityRouteGuard = EntityRouteGuard,\n> {\n /** Who may count: the guard the entity's `list` has, as a rule. */\n readonly guard: G;\n /** Defaults to `/api/<entity key>`; the route is `<basePath>/$facets`. */\n readonly basePath?: `/${string}`;\n /**\n * Narrows the counts to the records this caller may see — the same scope\n * the entity's `list` takes, so a count never reveals a record the list\n * would not.\n */\n readonly scope?: (\n context: EntityRouteContext,\n ) => Effect.Effect<EntityReadScope<T>, never, R>;\n}\n\n/**\n * `GET <base>/$facets` — how many of the entity's records hold each value of\n * each `facet` member, each under its own `rsql.<member>` filtering — bound to\n * storage through the host's {@link EntityFacetCounterFactoryTag}.\n *\n * A route of its own beside `makeEntityRoutes` rather than a sixth verb, so it\n * is the one route set that requires a counter: a host that mounts it without\n * binding one fails to compile, and a host that never counts binds nothing.\n *\n * The query is parsed against the entity's metadata, so only a `filterable`\n * member is counted and every malformed request is a `400`. A scope is\n * **conjoined into every facet's filtering**, never substituted, so no query\n * string can count past it; `'none'` answers every member empty.\n */\nexport const makeEntityFacetRoutes = <\n T extends Entity,\n R = never,\n // `never` for the reason `makeEntityRoutes` gives: a caller naming `R`\n // explicitly must not widen the guard to every kind there is.\n const G extends EntityRouteGuard = never,\n>(\n entityConstructor: EntityConstructor<T>,\n options: EntityFacetRoutesOptions<T, R, G>,\n) => {\n const base: `/${string}` =\n options.basePath ?? `/api/${envelopeEntityName(entityConstructor)}`;\n\n const readQueries = Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const search = new URL(request.url, 'http://localhost').searchParams;\n return yield* Effect.try({\n try: () => parseFacetQueryParams(entityConstructor, search),\n catch: error => error as EntifixBuildError,\n });\n });\n\n const scoped = (\n queries: EntityFacetQuery<T>[],\n scope: EntityReadScope<T>,\n ): EntityFacetQuery<T>[] =>\n scope === 'all' || scope === 'none'\n ? queries\n : queries.map(query => ({\n ...query,\n filtering: [...(query.filtering ?? []), scope.filter],\n }));\n\n const count = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const queries = yield* readQueries;\n const scope = options.scope\n ? yield* options.scope(context)\n : ('all' as const);\n if (scope === 'none') {\n // Still parsed: whether a query is well-formed must not depend on who\n // asked.\n const empty: EntityFacetCounts = Object.fromEntries(\n queries.map(({ member }) => [member, []]),\n );\n return yield* HttpServerResponse.json(\n makeEntityFacetsEnvelope(entityConstructor, empty),\n );\n }\n const factory = yield* EntityFacetCounterFactoryTag;\n const counter = yield* factory.for(entityConstructor, {\n ...(context.organizationId === undefined\n ? {}\n : { organizationId: context.organizationId }),\n });\n const counts = yield* facetCountsUCFactory<T>(\n scoped(queries, scope),\n ).pipe(Effect.provideService(EntityFacetCounterTag, counter));\n return yield* HttpServerResponse.json(\n makeEntityFacetsEnvelope(entityConstructor, counts),\n );\n }).pipe(\n Effect.catch(error => invalid(error, 'invalid query', 'invalidQuery')),\n );\n\n const handler = guardRoute(\n entityConstructor,\n options.guard,\n 'read',\n count,\n ) as Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n unknown,\n | R\n | EntityFacetCounterFactoryTag\n | GuardServices<G>\n | HttpServerRequest.HttpServerRequest\n | HttpRouter.RouteContext\n >;\n\n return HttpRouter.addAll([\n HttpRouter.route('GET', `${base}/$facets`, handler),\n ]);\n};\n"],"names":["EntityFacetCounterFactoryTag","EntityFacetCounterTag","facetCountsUCFactory","envelopeEntityName","makeEntityFacetsEnvelope","parseFacetQueryParams","Effect","HttpRouter","HttpServerRequest","HttpServerResponse","guardRoute","invalid","makeEntityFacetRoutes","entityConstructor","options","base","basePath","readQueries","gen","request","search","URL","url","searchParams","try","catch","error","scoped","queries","scope","map","query","filtering","filter","count","context","empty","Object","fromEntries","member","json","factory","counter","for","organizationId","undefined","counts","pipe","provideService","handler","guard","addAll","route"],"mappings":";AAAA,SACEA,4BAA4B,EAC5BC,qBAAqB,EACrBC,oBAAoB,QACf,oBAAoB;AAC3B,SAMEC,kBAAkB,EAClBC,wBAAwB,EACxBC,qBAAqB,QAChB,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAChC,SAASC,UAAU,EAAEC,iBAAiB,EAAEC,kBAAkB,QAAQ,cAAc;AAEhF,SAIEC,UAAU,QAEL,0BAA0B;AACjC,SAASC,OAAO,QAAQ,uBAAuB;AAqB/C;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,wBAAwB,CAOnCC,mBACAC;QAGEA;IADF,MAAMC,QACJD,oBAAAA,QAAQE,QAAQ,YAAhBF,oBAAoB,CAAC,KAAK,EAAEX,mBAAmBU,oBAAoB;IAErE,MAAMI,cAAcX,OAAOY,GAAG,CAAC;QAC7B,MAAMC,UAAU,OAAOX,kBAAkBA,iBAAiB;QAC1D,MAAMY,SAAS,IAAIC,IAAIF,QAAQG,GAAG,EAAE,oBAAoBC,YAAY;QACpE,OAAO,OAAOjB,OAAOkB,GAAG,CAAC;YACvBA,KAAK,IAAMnB,sBAAsBQ,mBAAmBO;YACpDK,OAAOC,CAAAA,QAASA;QAClB;IACF;IAEA,MAAMC,SAAS,CACbC,SACAC,QAEAA,UAAU,SAASA,UAAU,SACzBD,UACAA,QAAQE,GAAG,CAACC,CAAAA;gBAEMA;mBAFI,aACjBA;gBACHC,WAAW;wBAAKD,mBAAAA,MAAMC,SAAS,YAAfD,mBAAmB,EAAE;oBAAGF,MAAMI,MAAM;iBAAC;;;IAG7D,MAAMC,QAAQ,CAACC,UACb7B,OAAOY,GAAG,CAAC;YACT,MAAMU,UAAU,OAAOX;YACvB,MAAMY,QAAQf,QAAQe,KAAK,GACvB,OAAOf,QAAQe,KAAK,CAACM,WACpB;YACL,IAAIN,UAAU,QAAQ;gBACpB,sEAAsE;gBACtE,SAAS;gBACT,MAAMO,QAA2BC,OAAOC,WAAW,CACjDV,QAAQE,GAAG,CAAC,CAAC,EAAES,MAAM,EAAE,GAAK;wBAACA;wBAAQ,EAAE;qBAAC;gBAE1C,OAAO,OAAO9B,mBAAmB+B,IAAI,CACnCpC,yBAAyBS,mBAAmBuB;YAEhD;YACA,MAAMK,UAAU,OAAOzC;YACvB,MAAM0C,UAAU,OAAOD,QAAQE,GAAG,CAAC9B,mBAAmB,aAChDsB,QAAQS,cAAc,KAAKC,YAC3B,CAAC,IACD;gBAAED,gBAAgBT,QAAQS,cAAc;YAAC;YAE/C,MAAME,SAAS,OAAO5C,qBACpByB,OAAOC,SAASC,QAChBkB,IAAI,CAACzC,OAAO0C,cAAc,CAAC/C,uBAAuByC;YACpD,OAAO,OAAOjC,mBAAmB+B,IAAI,CACnCpC,yBAAyBS,mBAAmBiC;QAEhD,GAAGC,IAAI,CACLzC,OAAOmB,KAAK,CAACC,CAAAA,QAASf,QAAQe,OAAO,iBAAiB;IAG1D,MAAMuB,UAAUvC,WACdG,mBACAC,QAAQoC,KAAK,EACb,QACAhB;IAWF,OAAO3B,WAAW4C,MAAM,CAAC;QACvB5C,WAAW6C,KAAK,CAAC,OAAO,GAAGrC,KAAK,QAAQ,CAAC,EAAEkC;KAC5C;AACH,EAAE"}
1
+ {"version":3,"sources":["../src/entity-facet-routes.ts"],"sourcesContent":["import {\n EntityFacetCounterFactoryTag,\n EntityFacetCounterTag,\n facetCountsUCFactory,\n} from '@entifix/business';\nimport type { EntifixFailure } from '@entifix/core';\nimport {\n EntifixBuildError,\n type Entity,\n type EntityConstructor,\n type EntityFacetCounts,\n type EntityFacetQuery,\n envelopeEntityName,\n makeEntityFacetsEnvelope,\n parseFacetQueryParams,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';\n\nimport {\n type EntityReadScope,\n type EntityRouteContext,\n type EntityRouteGuard,\n guardRoute,\n type GuardServices,\n} from './entity-route-guard.js';\nimport {\n defaultFailurePolicy,\n type FailurePolicy,\n respond,\n} from './respond.js';\nimport { legacyAnswer } from './route-responses.js';\n\nexport interface EntityFacetRoutesOptions<\n T extends Entity,\n R,\n G extends EntityRouteGuard = EntityRouteGuard,\n> {\n /** Who may count: the guard the entity's `list` has, as a rule. */\n readonly guard: G;\n /** Defaults to `/api/<entity key>`; the route is `<basePath>/$facets`. */\n readonly basePath?: `/${string}`;\n /**\n * Narrows the counts to the records this caller may see — the same scope\n * the entity's `list` takes, so a count never reveals a record the list\n * would not.\n */\n readonly scope?: (\n context: EntityRouteContext,\n ) => Effect.Effect<EntityReadScope<T>, never, R>;\n /** How the count's failures are answered and logged. */\n readonly policy?: FailurePolicy;\n}\n\n/**\n * `GET <base>/$facets` — how many of the entity's records hold each value of\n * each `facet` member, each under its own `rsql.<member>` filtering — bound to\n * storage through the host's {@link EntityFacetCounterFactoryTag}.\n *\n * A route of its own beside `makeEntityRoutes` rather than a sixth verb, so it\n * is the one route set that requires a counter: a host that mounts it without\n * binding one fails to compile, and a host that never counts binds nothing.\n *\n * The query is parsed against the entity's metadata, so only a `filterable`\n * member is counted and every malformed request is a `400`. A scope is\n * **conjoined into every facet's filtering**, never substituted, so no query\n * string can count past it; `'none'` answers every member empty.\n */\nexport const makeEntityFacetRoutes = <\n T extends Entity,\n R = never,\n // `never` for the reason `makeEntityRoutes` gives: a caller naming `R`\n // explicitly must not widen the guard to every kind there is.\n const G extends EntityRouteGuard = never,\n>(\n entityConstructor: EntityConstructor<T>,\n options: EntityFacetRoutesOptions<T, R, G>,\n) => {\n const base: `/${string}` =\n options.basePath ?? `/api/${envelopeEntityName(entityConstructor)}`;\n\n const readQueries = Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const search = new URL(request.url, 'http://localhost').searchParams;\n return yield* Effect.try({\n try: () => parseFacetQueryParams(entityConstructor, search),\n catch: error => error as EntifixBuildError,\n });\n });\n\n const scoped = (\n queries: EntityFacetQuery<T>[],\n scope: EntityReadScope<T>,\n ): EntityFacetQuery<T>[] =>\n scope === 'all' || scope === 'none'\n ? queries\n : queries.map(query => ({\n ...query,\n filtering: [...(query.filtering ?? []), scope.filter],\n }));\n\n const count = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const queries = yield* readQueries;\n const scope = options.scope\n ? yield* options.scope(context)\n : ('all' as const);\n if (scope === 'none') {\n // Still parsed: whether a query is well-formed must not depend on who\n // asked.\n const empty: EntityFacetCounts = Object.fromEntries(\n queries.map(({ member }) => [member, []]),\n );\n return yield* HttpServerResponse.json(\n makeEntityFacetsEnvelope(entityConstructor, empty),\n );\n }\n const factory = yield* EntityFacetCounterFactoryTag;\n const counter = yield* factory.for(entityConstructor, {\n ...(context.organizationId === undefined\n ? {}\n : { organizationId: context.organizationId }),\n });\n const counts = yield* facetCountsUCFactory<T>(\n scoped(queries, scope),\n ).pipe(Effect.provideService(EntityFacetCounterTag, counter));\n return yield* HttpServerResponse.json(\n makeEntityFacetsEnvelope(entityConstructor, counts),\n );\n }).pipe(Effect.catch(legacyAnswer('invalidQuery')));\n\n const handler = respond(\n guardRoute(\n entityConstructor,\n options.guard,\n 'read',\n count,\n ) as Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n EntifixFailure,\n | R\n | EntityFacetCounterFactoryTag\n | GuardServices<G>\n | HttpServerRequest.HttpServerRequest\n | HttpRouter.RouteContext\n >,\n options.policy ?? defaultFailurePolicy,\n );\n\n return HttpRouter.addAll([\n HttpRouter.route('GET', `${base}/$facets`, handler),\n ]);\n};\n"],"names":["EntityFacetCounterFactoryTag","EntityFacetCounterTag","facetCountsUCFactory","envelopeEntityName","makeEntityFacetsEnvelope","parseFacetQueryParams","Effect","HttpRouter","HttpServerRequest","HttpServerResponse","guardRoute","defaultFailurePolicy","respond","legacyAnswer","makeEntityFacetRoutes","entityConstructor","options","base","basePath","readQueries","gen","request","search","URL","url","searchParams","try","catch","error","scoped","queries","scope","map","query","filtering","filter","count","context","empty","Object","fromEntries","member","json","factory","counter","for","organizationId","undefined","counts","pipe","provideService","handler","guard","policy","addAll","route"],"mappings":";AAAA,SACEA,4BAA4B,EAC5BC,qBAAqB,EACrBC,oBAAoB,QACf,oBAAoB;AAE3B,SAMEC,kBAAkB,EAClBC,wBAAwB,EACxBC,qBAAqB,QAChB,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAChC,SAASC,UAAU,EAAEC,iBAAiB,EAAEC,kBAAkB,QAAQ,cAAc;AAEhF,SAIEC,UAAU,QAEL,0BAA0B;AACjC,SACEC,oBAAoB,EAEpBC,OAAO,QACF,eAAe;AACtB,SAASC,YAAY,QAAQ,uBAAuB;AAuBpD;;;;;;;;;;;;;CAaC,GACD,OAAO,MAAMC,wBAAwB,CAOnCC,mBACAC;QAGEA,mBAmEAA;IApEF,MAAMC,QACJD,oBAAAA,QAAQE,QAAQ,YAAhBF,oBAAoB,CAAC,KAAK,EAAEb,mBAAmBY,oBAAoB;IAErE,MAAMI,cAAcb,OAAOc,GAAG,CAAC;QAC7B,MAAMC,UAAU,OAAOb,kBAAkBA,iBAAiB;QAC1D,MAAMc,SAAS,IAAIC,IAAIF,QAAQG,GAAG,EAAE,oBAAoBC,YAAY;QACpE,OAAO,OAAOnB,OAAOoB,GAAG,CAAC;YACvBA,KAAK,IAAMrB,sBAAsBU,mBAAmBO;YACpDK,OAAOC,CAAAA,QAASA;QAClB;IACF;IAEA,MAAMC,SAAS,CACbC,SACAC,QAEAA,UAAU,SAASA,UAAU,SACzBD,UACAA,QAAQE,GAAG,CAACC,CAAAA;gBAEMA;mBAFI,aACjBA;gBACHC,WAAW;wBAAKD,mBAAAA,MAAMC,SAAS,YAAfD,mBAAmB,EAAE;oBAAGF,MAAMI,MAAM;iBAAC;;;IAG7D,MAAMC,QAAQ,CAACC,UACb/B,OAAOc,GAAG,CAAC;YACT,MAAMU,UAAU,OAAOX;YACvB,MAAMY,QAAQf,QAAQe,KAAK,GACvB,OAAOf,QAAQe,KAAK,CAACM,WACpB;YACL,IAAIN,UAAU,QAAQ;gBACpB,sEAAsE;gBACtE,SAAS;gBACT,MAAMO,QAA2BC,OAAOC,WAAW,CACjDV,QAAQE,GAAG,CAAC,CAAC,EAAES,MAAM,EAAE,GAAK;wBAACA;wBAAQ,EAAE;qBAAC;gBAE1C,OAAO,OAAOhC,mBAAmBiC,IAAI,CACnCtC,yBAAyBW,mBAAmBuB;YAEhD;YACA,MAAMK,UAAU,OAAO3C;YACvB,MAAM4C,UAAU,OAAOD,QAAQE,GAAG,CAAC9B,mBAAmB,aAChDsB,QAAQS,cAAc,KAAKC,YAC3B,CAAC,IACD;gBAAED,gBAAgBT,QAAQS,cAAc;YAAC;YAE/C,MAAME,SAAS,OAAO9C,qBACpB2B,OAAOC,SAASC,QAChBkB,IAAI,CAAC3C,OAAO4C,cAAc,CAACjD,uBAAuB2C;YACpD,OAAO,OAAOnC,mBAAmBiC,IAAI,CACnCtC,yBAAyBW,mBAAmBiC;QAEhD,GAAGC,IAAI,CAAC3C,OAAOqB,KAAK,CAACd,aAAa;IAEpC,MAAMsC,UAAUvC,QACdF,WACEK,mBACAC,QAAQoC,KAAK,EACb,QACAhB,SAUFpB,kBAAAA,QAAQqC,MAAM,YAAdrC,kBAAkBL;IAGpB,OAAOJ,WAAW+C,MAAM,CAAC;QACvB/C,WAAWgD,KAAK,CAAC,OAAO,GAAGtC,KAAK,QAAQ,CAAC,EAAEkC;KAC5C;AACH,EAAE"}
@@ -20,5 +20,5 @@ import { HttpServerRequest, HttpServerResponse } from 'effect/http';
20
20
  * database handle here would leave a vendor with no active organization unable
21
21
  * to see their own affordances.
22
22
  */
23
- export declare const entityMetadataRoute: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>) => Effect.Effect<HttpServerResponse.HttpServerResponse, import("effect/http/HttpBody").HttpBodyError, import("@entifix/business").TokenServiceTag | PolicyDecisionTag | HttpServerRequest.HttpServerRequest>;
23
+ export declare const entityMetadataRoute: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>) => Effect.Effect<HttpServerResponse.HttpServerResponse, never, import("@entifix/business").TokenServiceTag | PolicyDecisionTag | HttpServerRequest.HttpServerRequest>;
24
24
  //# sourceMappingURL=entity-metadata-route.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity-metadata-route.d.ts","sourceRoot":"","sources":["../src/entity-metadata-route.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAEL,KAAK,MAAM,EAEX,KAAK,iBAAiB,EAIvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAIpE;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,SAAS,MAAM,EACxD,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,8MA0D5C,CAAC"}
1
+ {"version":3,"file":"entity-metadata-route.d.ts","sourceRoot":"","sources":["../src/entity-metadata-route.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAEL,KAAK,MAAM,EAEX,KAAK,iBAAiB,EAKvB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAKpE;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,SAAS,MAAM,EACxD,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,uKA6D5C,CAAC"}
@@ -1,9 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { parsePermission, permissionForEntity, permissionForUseCase, PolicyDecisionTag } from '@entifix/authz';
3
- import { describeEntityUseCases, ENTITY_ACTIONS, extractMetaEntity, makeEntityMetadataEnvelope } from '@entifix/core';
3
+ import { describeEntityUseCases, ENTITY_ACTIONS, extractMetaEntity, makeEntityMetadataEnvelope, NotFound } from '@entifix/core';
4
4
  import { Effect } from 'effect';
5
5
  import { HttpServerRequest, HttpServerResponse } from 'effect/http';
6
6
  import { requirePrincipal } from './auth/index.js';
7
+ import { respond } from './respond.js';
7
8
  /**
8
9
  * `GET /api/<entity>/$metadata` — what this caller may do with one entity.
9
10
  *
@@ -21,7 +22,7 @@ import { requirePrincipal } from './auth/index.js';
21
22
  * service: the document describes the model, not tenant data, and resolving a
22
23
  * database handle here would leave a vendor with no active organization unable
23
24
  * to see their own affordances.
24
- */ export const entityMetadataRoute = (entityConstructor)=>requirePrincipal((principal)=>Effect.gen(function*() {
25
+ */ export const entityMetadataRoute = (entityConstructor)=>respond(requirePrincipal((principal)=>Effect.gen(function*() {
25
26
  const policy = yield* PolicyDecisionTag;
26
27
  const allows = (permission)=>{
27
28
  const { resource, action } = parsePermission(permission);
@@ -66,7 +67,7 @@ import { requirePrincipal } from './auth/index.js';
66
67
  contentType: 'application/json',
67
68
  headers: cacheHeaders(etag)
68
69
  });
69
- }));
70
+ })));
70
71
  /**
71
72
  * The response varies by principal, so it must never land in a shared cache.
72
73
  * `Vary` names **both** credential carriers because the shell reads the access
@@ -77,12 +78,8 @@ import { requirePrincipal } from './auth/index.js';
77
78
  'cache-control': 'private, no-cache',
78
79
  vary: 'Cookie, Authorization'
79
80
  });
80
- /** Indistinguishable from the `404` a service gives for an entity it does not host. */ const notFound = (entityConstructor)=>HttpServerResponse.json({
81
- message: 'not found',
82
- code: 'notFound',
81
+ /** Indistinguishable from the `404` a service gives for an entity it does not host. */ const notFound = (entityConstructor)=>Effect.fail(new NotFound({
83
82
  entity: extractMetaEntity(entityConstructor).key
84
- }, {
85
- status: 404
86
- });
83
+ }));
87
84
 
88
85
  //# sourceMappingURL=entity-metadata-route.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/entity-metadata-route.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\nimport {\n parsePermission,\n type Permission,\n permissionForEntity,\n permissionForUseCase,\n PolicyDecisionTag,\n} from '@entifix/authz';\nimport {\n describeEntityUseCases,\n type Entity,\n ENTITY_ACTIONS,\n type EntityConstructor,\n type EntityMetadataDocument,\n extractMetaEntity,\n makeEntityMetadataEnvelope,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport { HttpServerRequest, HttpServerResponse } from 'effect/http';\n\nimport { requirePrincipal } from './auth/index.js';\n\n/**\n * `GET /api/<entity>/$metadata` — what this caller may do with one entity.\n *\n * Mounted **per entity, at a literal path**, not as a `/api/:entity/$metadata`\n * wildcard. `HttpRouter` resolves through `find-my-way-ts`, where a static\n * segment beats a parametric one and there is no backtracking once the\n * parametric branch has matched: alongside an existing `/api/<entity>/:id`, a\n * wildcard `$metadata` route never runs at all — the by-id handler wins with\n * `id === \"$metadata\"`, misses, and answers its own `404`. The endpoint would\n * read as \"this entity has no metadata\" while appearing mounted. A literal wins\n * regardless of registration order, which is why the base exports a route for\n * one entity and each service registers it for each of its own.\n *\n * Deliberately **not** behind `requireOrganization`, even on a tenant-plane\n * service: the document describes the model, not tenant data, and resolving a\n * database handle here would leave a vendor with no active organization unable\n * to see their own affordances.\n */\nexport const entityMetadataRoute = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n) =>\n requirePrincipal(principal =>\n Effect.gen(function* () {\n const policy = yield* PolicyDecisionTag;\n const allows = (permission: Permission): boolean => {\n const { resource, action } = parsePermission(permission);\n return policy.decide({\n subject: { roles: principal.roles },\n resource,\n action,\n });\n };\n\n // SECURITY BOUNDARY. The document names domains, entity keys and verbs —\n // it is a map of the model — so an entity the caller may not read answers\n // exactly as one this service does not host. A `403` here would make the\n // endpoint an oracle: walk it and learn the shape of everything you are\n // not allowed to see. Same category as `redactConfiguration`, not\n // diagnostics polish.\n if (!allows(permissionForEntity(entityConstructor, 'read'))) {\n return yield* notFound(entityConstructor);\n }\n\n const document: EntityMetadataDocument = {\n actions: ENTITY_ACTIONS.filter(action =>\n allows(permissionForEntity(entityConstructor, action)),\n ),\n useCases: describeEntityUseCases(entityConstructor).filter(descriptor =>\n allows(permissionForUseCase(entityConstructor, descriptor.key)),\n ),\n };\n\n // The ETag hashes the *computed* document, not the descriptor set the\n // deployment declares. Two principals with different grants get different\n // documents from the same model, so a hash of the model would `304` one\n // caller onto another's affordances. Hashing the permission set instead\n // would read `ROLE_PERMISSIONS` directly and go around `PolicyDecisionTag`,\n // so an injected ABAC policy could change affordances without changing the\n // tag. Computing first costs a metadata read and a handful of pure\n // `decide` calls — no IO.\n const body = JSON.stringify(\n makeEntityMetadataEnvelope(entityConstructor, document),\n );\n const etag = `\"${createHash('sha256').update(body).digest('hex')}\"`;\n\n const request = yield* HttpServerRequest.HttpServerRequest;\n if (request.headers['if-none-match'] === etag) {\n return HttpServerResponse.empty({ status: 304 }).pipe(\n HttpServerResponse.setHeaders(cacheHeaders(etag)),\n );\n }\n\n return HttpServerResponse.text(body, {\n contentType: 'application/json',\n headers: cacheHeaders(etag),\n });\n }),\n );\n\n/**\n * The response varies by principal, so it must never land in a shared cache.\n * `Vary` names **both** credential carriers because the shell reads the access\n * token from the `entifix_at` cookie *or* an `Authorization: Bearer` header, and a\n * cache keyed on only one of them would serve across the other.\n */\nconst cacheHeaders = (etag: string) => ({\n etag,\n 'cache-control': 'private, no-cache',\n vary: 'Cookie, Authorization',\n});\n\n/** Indistinguishable from the `404` a service gives for an entity it does not host. */\nconst notFound = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n) =>\n HttpServerResponse.json(\n {\n message: 'not found',\n code: 'notFound',\n entity: extractMetaEntity(entityConstructor).key,\n },\n { status: 404 },\n );\n"],"names":["createHash","parsePermission","permissionForEntity","permissionForUseCase","PolicyDecisionTag","describeEntityUseCases","ENTITY_ACTIONS","extractMetaEntity","makeEntityMetadataEnvelope","Effect","HttpServerRequest","HttpServerResponse","requirePrincipal","entityMetadataRoute","entityConstructor","principal","gen","policy","allows","permission","resource","action","decide","subject","roles","notFound","document","actions","filter","useCases","descriptor","key","body","JSON","stringify","etag","update","digest","request","headers","empty","status","pipe","setHeaders","cacheHeaders","text","contentType","vary","json","message","code","entity"],"mappings":"AAAA,SAASA,UAAU,QAAQ,cAAc;AAEzC,SACEC,eAAe,EAEfC,mBAAmB,EACnBC,oBAAoB,EACpBC,iBAAiB,QACZ,iBAAiB;AACxB,SACEC,sBAAsB,EAEtBC,cAAc,EAGdC,iBAAiB,EACjBC,0BAA0B,QACrB,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAChC,SAASC,iBAAiB,EAAEC,kBAAkB,QAAQ,cAAc;AAEpE,SAASC,gBAAgB,QAAQ,kBAAkB;AAEnD;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAMC,sBAAsB,CACjCC,oBAEAF,iBAAiBG,CAAAA,YACfN,OAAOO,GAAG,CAAC;YACT,MAAMC,SAAS,OAAOb;YACtB,MAAMc,SAAS,CAACC;gBACd,MAAM,EAAEC,QAAQ,EAAEC,MAAM,EAAE,GAAGpB,gBAAgBkB;gBAC7C,OAAOF,OAAOK,MAAM,CAAC;oBACnBC,SAAS;wBAAEC,OAAOT,UAAUS,KAAK;oBAAC;oBAClCJ;oBACAC;gBACF;YACF;YAEA,yEAAyE;YACzE,0EAA0E;YAC1E,yEAAyE;YACzE,wEAAwE;YACxE,kEAAkE;YAClE,sBAAsB;YACtB,IAAI,CAACH,OAAOhB,oBAAoBY,mBAAmB,UAAU;gBAC3D,OAAO,OAAOW,SAASX;YACzB;YAEA,MAAMY,WAAmC;gBACvCC,SAASrB,eAAesB,MAAM,CAACP,CAAAA,SAC7BH,OAAOhB,oBAAoBY,mBAAmBO;gBAEhDQ,UAAUxB,uBAAuBS,mBAAmBc,MAAM,CAACE,CAAAA,aACzDZ,OAAOf,qBAAqBW,mBAAmBgB,WAAWC,GAAG;YAEjE;YAEA,sEAAsE;YACtE,0EAA0E;YAC1E,wEAAwE;YACxE,wEAAwE;YACxE,4EAA4E;YAC5E,2EAA2E;YAC3E,mEAAmE;YACnE,0BAA0B;YAC1B,MAAMC,OAAOC,KAAKC,SAAS,CACzB1B,2BAA2BM,mBAAmBY;YAEhD,MAAMS,OAAO,CAAC,CAAC,EAAEnC,WAAW,UAAUoC,MAAM,CAACJ,MAAMK,MAAM,CAAC,OAAO,CAAC,CAAC;YAEnE,MAAMC,UAAU,OAAO5B,kBAAkBA,iBAAiB;YAC1D,IAAI4B,QAAQC,OAAO,CAAC,gBAAgB,KAAKJ,MAAM;gBAC7C,OAAOxB,mBAAmB6B,KAAK,CAAC;oBAAEC,QAAQ;gBAAI,GAAGC,IAAI,CACnD/B,mBAAmBgC,UAAU,CAACC,aAAaT;YAE/C;YAEA,OAAOxB,mBAAmBkC,IAAI,CAACb,MAAM;gBACnCc,aAAa;gBACbP,SAASK,aAAaT;YACxB;QACF,IACA;AAEJ;;;;;CAKC,GACD,MAAMS,eAAe,CAACT,OAAkB,CAAA;QACtCA;QACA,iBAAiB;QACjBY,MAAM;IACR,CAAA;AAEA,qFAAqF,GACrF,MAAMtB,WAAW,CACfX,oBAEAH,mBAAmBqC,IAAI,CACrB;QACEC,SAAS;QACTC,MAAM;QACNC,QAAQ5C,kBAAkBO,mBAAmBiB,GAAG;IAClD,GACA;QAAEU,QAAQ;IAAI"}
1
+ {"version":3,"sources":["../src/entity-metadata-route.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\nimport {\n parsePermission,\n type Permission,\n permissionForEntity,\n permissionForUseCase,\n PolicyDecisionTag,\n} from '@entifix/authz';\nimport {\n describeEntityUseCases,\n type Entity,\n ENTITY_ACTIONS,\n type EntityConstructor,\n type EntityMetadataDocument,\n extractMetaEntity,\n makeEntityMetadataEnvelope,\n NotFound,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport { HttpServerRequest, HttpServerResponse } from 'effect/http';\n\nimport { requirePrincipal } from './auth/index.js';\nimport { respond } from './respond.js';\n\n/**\n * `GET /api/<entity>/$metadata` — what this caller may do with one entity.\n *\n * Mounted **per entity, at a literal path**, not as a `/api/:entity/$metadata`\n * wildcard. `HttpRouter` resolves through `find-my-way-ts`, where a static\n * segment beats a parametric one and there is no backtracking once the\n * parametric branch has matched: alongside an existing `/api/<entity>/:id`, a\n * wildcard `$metadata` route never runs at all — the by-id handler wins with\n * `id === \"$metadata\"`, misses, and answers its own `404`. The endpoint would\n * read as \"this entity has no metadata\" while appearing mounted. A literal wins\n * regardless of registration order, which is why the base exports a route for\n * one entity and each service registers it for each of its own.\n *\n * Deliberately **not** behind `requireOrganization`, even on a tenant-plane\n * service: the document describes the model, not tenant data, and resolving a\n * database handle here would leave a vendor with no active organization unable\n * to see their own affordances.\n */\nexport const entityMetadataRoute = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n) =>\n respond(\n requirePrincipal(principal =>\n Effect.gen(function* () {\n const policy = yield* PolicyDecisionTag;\n const allows = (permission: Permission): boolean => {\n const { resource, action } = parsePermission(permission);\n return policy.decide({\n subject: { roles: principal.roles },\n resource,\n action,\n });\n };\n\n // SECURITY BOUNDARY. The document names domains, entity keys and verbs —\n // it is a map of the model — so an entity the caller may not read answers\n // exactly as one this service does not host. A `403` here would make the\n // endpoint an oracle: walk it and learn the shape of everything you are\n // not allowed to see. Same category as `redactConfiguration`, not\n // diagnostics polish.\n if (!allows(permissionForEntity(entityConstructor, 'read'))) {\n return yield* notFound(entityConstructor);\n }\n\n const document: EntityMetadataDocument = {\n actions: ENTITY_ACTIONS.filter(action =>\n allows(permissionForEntity(entityConstructor, action)),\n ),\n useCases: describeEntityUseCases(entityConstructor).filter(\n descriptor =>\n allows(permissionForUseCase(entityConstructor, descriptor.key)),\n ),\n };\n\n // The ETag hashes the *computed* document, not the descriptor set the\n // deployment declares. Two principals with different grants get different\n // documents from the same model, so a hash of the model would `304` one\n // caller onto another's affordances. Hashing the permission set instead\n // would read `ROLE_PERMISSIONS` directly and go around `PolicyDecisionTag`,\n // so an injected ABAC policy could change affordances without changing the\n // tag. Computing first costs a metadata read and a handful of pure\n // `decide` calls — no IO.\n const body = JSON.stringify(\n makeEntityMetadataEnvelope(entityConstructor, document),\n );\n const etag = `\"${createHash('sha256').update(body).digest('hex')}\"`;\n\n const request = yield* HttpServerRequest.HttpServerRequest;\n if (request.headers['if-none-match'] === etag) {\n return HttpServerResponse.empty({ status: 304 }).pipe(\n HttpServerResponse.setHeaders(cacheHeaders(etag)),\n );\n }\n\n return HttpServerResponse.text(body, {\n contentType: 'application/json',\n headers: cacheHeaders(etag),\n });\n }),\n ),\n );\n\n/**\n * The response varies by principal, so it must never land in a shared cache.\n * `Vary` names **both** credential carriers because the shell reads the access\n * token from the `entifix_at` cookie *or* an `Authorization: Bearer` header, and a\n * cache keyed on only one of them would serve across the other.\n */\nconst cacheHeaders = (etag: string) => ({\n etag,\n 'cache-control': 'private, no-cache',\n vary: 'Cookie, Authorization',\n});\n\n/** Indistinguishable from the `404` a service gives for an entity it does not host. */\nconst notFound = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n) =>\n Effect.fail(\n new NotFound({ entity: extractMetaEntity(entityConstructor).key }),\n );\n"],"names":["createHash","parsePermission","permissionForEntity","permissionForUseCase","PolicyDecisionTag","describeEntityUseCases","ENTITY_ACTIONS","extractMetaEntity","makeEntityMetadataEnvelope","NotFound","Effect","HttpServerRequest","HttpServerResponse","requirePrincipal","respond","entityMetadataRoute","entityConstructor","principal","gen","policy","allows","permission","resource","action","decide","subject","roles","notFound","document","actions","filter","useCases","descriptor","key","body","JSON","stringify","etag","update","digest","request","headers","empty","status","pipe","setHeaders","cacheHeaders","text","contentType","vary","fail","entity"],"mappings":"AAAA,SAASA,UAAU,QAAQ,cAAc;AAEzC,SACEC,eAAe,EAEfC,mBAAmB,EACnBC,oBAAoB,EACpBC,iBAAiB,QACZ,iBAAiB;AACxB,SACEC,sBAAsB,EAEtBC,cAAc,EAGdC,iBAAiB,EACjBC,0BAA0B,EAC1BC,QAAQ,QACH,gBAAgB;AACvB,SAASC,MAAM,QAAQ,SAAS;AAChC,SAASC,iBAAiB,EAAEC,kBAAkB,QAAQ,cAAc;AAEpE,SAASC,gBAAgB,QAAQ,kBAAkB;AACnD,SAASC,OAAO,QAAQ,eAAe;AAEvC;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAMC,sBAAsB,CACjCC,oBAEAF,QACED,iBAAiBI,CAAAA,YACfP,OAAOQ,GAAG,CAAC;YACT,MAAMC,SAAS,OAAOf;YACtB,MAAMgB,SAAS,CAACC;gBACd,MAAM,EAAEC,QAAQ,EAAEC,MAAM,EAAE,GAAGtB,gBAAgBoB;gBAC7C,OAAOF,OAAOK,MAAM,CAAC;oBACnBC,SAAS;wBAAEC,OAAOT,UAAUS,KAAK;oBAAC;oBAClCJ;oBACAC;gBACF;YACF;YAEA,yEAAyE;YACzE,0EAA0E;YAC1E,yEAAyE;YACzE,wEAAwE;YACxE,kEAAkE;YAClE,sBAAsB;YACtB,IAAI,CAACH,OAAOlB,oBAAoBc,mBAAmB,UAAU;gBAC3D,OAAO,OAAOW,SAASX;YACzB;YAEA,MAAMY,WAAmC;gBACvCC,SAASvB,eAAewB,MAAM,CAACP,CAAAA,SAC7BH,OAAOlB,oBAAoBc,mBAAmBO;gBAEhDQ,UAAU1B,uBAAuBW,mBAAmBc,MAAM,CACxDE,CAAAA,aACEZ,OAAOjB,qBAAqBa,mBAAmBgB,WAAWC,GAAG;YAEnE;YAEA,sEAAsE;YACtE,0EAA0E;YAC1E,wEAAwE;YACxE,wEAAwE;YACxE,4EAA4E;YAC5E,2EAA2E;YAC3E,mEAAmE;YACnE,0BAA0B;YAC1B,MAAMC,OAAOC,KAAKC,SAAS,CACzB5B,2BAA2BQ,mBAAmBY;YAEhD,MAAMS,OAAO,CAAC,CAAC,EAAErC,WAAW,UAAUsC,MAAM,CAACJ,MAAMK,MAAM,CAAC,OAAO,CAAC,CAAC;YAEnE,MAAMC,UAAU,OAAO7B,kBAAkBA,iBAAiB;YAC1D,IAAI6B,QAAQC,OAAO,CAAC,gBAAgB,KAAKJ,MAAM;gBAC7C,OAAOzB,mBAAmB8B,KAAK,CAAC;oBAAEC,QAAQ;gBAAI,GAAGC,IAAI,CACnDhC,mBAAmBiC,UAAU,CAACC,aAAaT;YAE/C;YAEA,OAAOzB,mBAAmBmC,IAAI,CAACb,MAAM;gBACnCc,aAAa;gBACbP,SAASK,aAAaT;YACxB;QACF,KAEF;AAEJ;;;;;CAKC,GACD,MAAMS,eAAe,CAACT,OAAkB,CAAA;QACtCA;QACA,iBAAiB;QACjBY,MAAM;IACR,CAAA;AAEA,qFAAqF,GACrF,MAAMtB,WAAW,CACfX,oBAEAN,OAAOwC,IAAI,CACT,IAAIzC,SAAS;QAAE0C,QAAQ5C,kBAAkBS,mBAAmBiB,GAAG;IAAC"}
@@ -56,5 +56,5 @@ export type GuardServices<G extends EntityRouteGuard> = TokenServiceTag | Policy
56
56
  * check that enforces it, so the entity routes and the facet route cannot
57
57
  * enforce the same guard differently.
58
58
  */
59
- export declare const guardRoute: <T extends Entity, A, E, R>(entityConstructor: EntityConstructor<T>, guard: EntityRouteGuard, action: Action, use: (context: EntityRouteContext) => Effect.Effect<A, E, R>) => Effect.Effect<import("effect/http/HttpServerResponse").HttpServerResponse | A, import("effect/http/HttpBody").HttpBodyError | E, TokenServiceTag | PolicyDecisionTag | import("effect/http/HttpServerRequest").HttpServerRequest | R> | Effect.Effect<import("effect/http/HttpServerResponse").HttpServerResponse | A, import("effect/http/HttpBody").HttpBodyError | E, ServiceCrossingPolicyTag | import("effect/http/HttpServerRequest").HttpServerRequest | ServiceCrossingTokenTag | R>;
59
+ export declare const guardRoute: <T extends Entity, A, E, R>(entityConstructor: EntityConstructor<T>, guard: EntityRouteGuard, action: Action, use: (context: EntityRouteContext) => Effect.Effect<A, E, R>) => Effect.Effect<import("effect/http/HttpServerResponse").HttpServerResponse | A, E, TokenServiceTag | PolicyDecisionTag | import("effect/http/HttpServerRequest").HttpServerRequest | R> | Effect.Effect<import("effect/http/HttpServerResponse").HttpServerResponse | A, E, ServiceCrossingPolicyTag | import("effect/http/HttpServerRequest").HttpServerRequest | ServiceCrossingTokenTag | R>;
60
60
  //# sourceMappingURL=entity-route-guard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"entity-route-guard.d.ts","sourceRoot":"","sources":["../src/entity-route-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC9B,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,EACL,KAAK,gBAAgB,EAKrB,KAAK,uBAAuB,EAC7B,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GACjC;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC;AAElE,4DAA4D;AAC5D,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;CACvC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,MAAM,IACxC,KAAK,GACL,MAAM,GACN;IACE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;CAC3C,CAAC;AAEN;;;;GAIG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,gBAAgB,IAChD,eAAe,GACf,iBAAiB,GACjB,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACpC,uBAAuB,GAAG,wBAAwB,GAClD,KAAK,CAAC,CAAC;AAEf;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAClD,mBAAmB,iBAAiB,CAAC,CAAC,CAAC,EACvC,OAAO,gBAAgB,EACvB,QAAQ,MAAM,EACd,KAAK,CAAC,OAAO,EAAE,kBAAkB,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,ieAmB7D,CAAC"}
1
+ {"version":3,"file":"entity-route-guard.d.ts","sourceRoot":"","sources":["../src/entity-route-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,MAAM,EAEX,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC9B,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,EACL,KAAK,gBAAgB,EAKrB,KAAK,uBAAuB,EAC7B,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC3B;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GACjC;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC;AAElE,4DAA4D;AAC5D,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,CAAC;CACvC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,MAAM,IACxC,KAAK,GACL,MAAM,GACN;IACE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,OAAO,CAAC;CAC3C,CAAC;AAEN;;;;GAIG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,gBAAgB,IAChD,eAAe,GACf,iBAAiB,GACjB,CAAC,CAAC,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACpC,uBAAuB,GAAG,wBAAwB,GAClD,KAAK,CAAC,CAAC;AAEf;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAClD,mBAAmB,iBAAiB,CAAC,CAAC,CAAC,EACvC,OAAO,gBAAgB,EACvB,QAAQ,MAAM,EACd,KAAK,CAAC,OAAO,EAAE,kBAAkB,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,mYAmB7D,CAAC"}
@@ -1,9 +1,10 @@
1
1
  import { EntityRepositoryFactoryTag } from '@entifix/business';
2
- import { EntifixBuildError, type EntifixEnvelopeLink, type Entity, type EntityConstructor, type EntityId } from '@entifix/core';
2
+ import { type EntifixBuildError, type EntifixEnvelopeLink, type Entity, type EntityConstructor, type EntityId } from '@entifix/core';
3
3
  import { Effect } from 'effect';
4
4
  import { HttpRouter, HttpServerResponse } from 'effect/http';
5
5
  import { type EntityReadScope, type EntityRouteContext, type EntityRouteGuard } from './entity-route-guard.js';
6
- import { EntityRouteRefusal, routeFailure } from './route-responses.js';
6
+ import { type FailurePolicy } from './respond.js';
7
+ import { type EntityRouteRefusal, routeFailure } from './route-responses.js';
7
8
  /** The five generic verbs a route set can mount. */
8
9
  export type EntityVerb = 'list' | 'get' | 'create' | 'update' | 'delete';
9
10
  export { type EntityReadScope, type EntityRouteContext, type EntityRouteGuard, } from './entity-route-guard.js';
@@ -39,6 +40,11 @@ export interface EntityRoutesOptions<T extends Entity, R, G extends EntityRouteG
39
40
  * cannot drift from the rest of the set.
40
41
  */
41
42
  readonly override?: Partial<Record<EntityVerb, EntityRouteOverride<R>>>;
43
+ /**
44
+ * How every verb's failures are answered and logged. Defaults to
45
+ * {@link defaultFailurePolicy}; a host passes its fleet policy.
46
+ */
47
+ readonly policy?: FailurePolicy;
42
48
  }
43
49
  /**
44
50
  * {@link deriveEntityRoutes}, with a curried form for a caller that has to
@@ -48,10 +54,10 @@ export interface EntityRoutesOptions<T extends Entity, R, G extends EntityRouteG
48
54
  * arguments cannot do.
49
55
  */
50
56
  export { routeFailure };
51
- export declare const makeEntityRoutes: (<T extends Entity, R = never, const G extends EntityRouteGuard = never>(entityConstructor: EntityConstructor<T>, options: EntityRoutesOptions<T, R, G>) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", import("@entifix/business").TokenServiceTag> | HttpRouter.Request<"Requires", import("@entifix/authz").PolicyDecisionTag> | HttpRouter.Request<"Error", unknown> | HttpRouter.Request<"Requires", EntityRepositoryFactoryTag> | HttpRouter.Request.From<"Requires", Exclude<Exclude<R, HttpRouter.Provided>, HttpRouter.Provided>> | HttpRouter.Request.From<"Requires", Exclude<Exclude<G extends {
57
+ export declare const makeEntityRoutes: (<T extends Entity, R = never, const G extends EntityRouteGuard = never>(entityConstructor: EntityConstructor<T>, options: EntityRoutesOptions<T, R, G>) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", import("@entifix/business").TokenServiceTag> | HttpRouter.Request<"Requires", import("@entifix/authz").PolicyDecisionTag> | HttpRouter.Request<"Requires", EntityRepositoryFactoryTag> | HttpRouter.Request.From<"Requires", Exclude<Exclude<R, HttpRouter.Provided>, HttpRouter.Provided>> | HttpRouter.Request.From<"Requires", Exclude<Exclude<G extends {
52
58
  readonly kind: "crossing";
53
59
  } ? import("@entifix/authz").ServiceCrossingPolicyTag | import("./index.js").ServiceCrossingTokenTag : never, HttpRouter.Provided>, HttpRouter.Provided>>>) & {
54
- for: <R>() => <T extends Entity, const G extends EntityRouteGuard>(entityConstructor: EntityConstructor<T>, options: EntityRoutesOptions<T, R, G>) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", import("@entifix/business").TokenServiceTag> | HttpRouter.Request<"Requires", import("@entifix/authz").PolicyDecisionTag> | HttpRouter.Request<"Error", unknown> | HttpRouter.Request<"Requires", EntityRepositoryFactoryTag> | HttpRouter.Request.From<"Requires", Exclude<Exclude<R, HttpRouter.Provided>, HttpRouter.Provided>> | HttpRouter.Request.From<"Requires", Exclude<Exclude<G extends {
60
+ for: <R>() => <T extends Entity, const G extends EntityRouteGuard>(entityConstructor: EntityConstructor<T>, options: EntityRoutesOptions<T, R, G>) => import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Requires", import("@entifix/business").TokenServiceTag> | HttpRouter.Request<"Requires", import("@entifix/authz").PolicyDecisionTag> | HttpRouter.Request<"Requires", EntityRepositoryFactoryTag> | HttpRouter.Request.From<"Requires", Exclude<Exclude<R, HttpRouter.Provided>, HttpRouter.Provided>> | HttpRouter.Request.From<"Requires", Exclude<Exclude<G extends {
55
61
  readonly kind: "crossing";
56
62
  } ? import("@entifix/authz").ServiceCrossingPolicyTag | import("./index.js").ServiceCrossingTokenTag : never, HttpRouter.Provided>, HttpRouter.Provided>>>;
57
63
  };
@@ -1 +1 @@
1
- {"version":3,"file":"entity-routes.d.ts","sourceRoot":"","sources":["../src/entity-routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"entity-routes.d.ts","sourceRoot":"","sources":["../src/entity-routes.ts"],"names":[],"mappings":"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"}
@@ -1,11 +1,12 @@
1
1
  import { _ as _extends } from "@swc/helpers/_/_extends";
2
2
  import { deleteUCFactory, EntityIdTag, EntityLoadRequestTag, EntityRepositoryFactoryTag, EntityRepositoryTag, EntityTag, getUCFactory, loadUCFactory, saveUCFactory } from '@entifix/business';
3
- import { envelopeEntityName, extractMetaEntity, makeEntityEnvelope, makeEntityPageEnvelope, parseLoadRequestParams, readEntityEnvelope } from '@entifix/core';
3
+ import { envelopeEntityName, extractMetaEntity, makeEntityEnvelope, makeEntityPageEnvelope, NotFound, parseLoadRequestParams, readEntityEnvelope } from '@entifix/core';
4
4
  import { Effect } from 'effect';
5
5
  import { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';
6
6
  import { entityMetadataRoute } from './entity-metadata-route.js';
7
7
  import { guardRoute } from './entity-route-guard.js';
8
- import { invalid, routeFailure } from './route-responses.js';
8
+ import { defaultFailurePolicy, respond } from './respond.js';
9
+ import { legacyAnswer, routeFailure } from './route-responses.js';
9
10
  export { EntityRouteRefusal } from './route-responses.js';
10
11
  const ACTION = {
11
12
  list: 'read',
@@ -108,13 +109,9 @@ const READS = new Set([
108
109
  catch: (error)=>error
109
110
  });
110
111
  });
111
- const notFound = HttpServerResponse.json({
112
- message: 'not found',
113
- code: 'notFound',
112
+ const notFound = Effect.fail(new NotFound({
114
113
  entity: extractMetaEntity(entityConstructor).key
115
- }, {
116
- status: 404
117
- });
114
+ }));
118
115
  const list = (context)=>Effect.gen(function*() {
119
116
  var _parsed_filtering;
120
117
  const parsed = yield* readLoadRequest;
@@ -139,7 +136,7 @@ const READS = new Set([
139
136
  });
140
137
  const page = yield* loadUCFactory().pipe(Effect.provideService(EntityRepositoryTag, (yield* repository(context))), Effect.provideService(EntityLoadRequestTag, request));
141
138
  return yield* HttpServerResponse.json(makeEntityPageEnvelope(entityConstructor, page, links.collection()));
142
- }).pipe(Effect.catch((error)=>invalid(error, 'invalid query', 'invalidQuery')));
139
+ }).pipe(Effect.catch(legacyAnswer('invalidQuery')));
143
140
  const get = (context)=>Effect.gen(function*() {
144
141
  const params = yield* HttpRouter.params;
145
142
  const scope = yield* scopeOf(context);
@@ -151,7 +148,7 @@ const READS = new Set([
151
148
  return yield* notFound;
152
149
  }
153
150
  return yield* HttpServerResponse.json(makeEntityEnvelope(entityConstructor, found.value, links.record(params.id)));
154
- }).pipe(Effect.catch(routeFailure));
151
+ }).pipe(Effect.catch(legacyAnswer('invalid')));
155
152
  const save = (verb)=>(context)=>Effect.gen(function*() {
156
153
  const request = yield* HttpServerRequest.HttpServerRequest;
157
154
  const body = yield* request.json;
@@ -169,7 +166,7 @@ const READS = new Set([
169
166
  }
170
167
  const saved = yield* saveUCFactory().pipe(Effect.provideService(EntityRepositoryTag, (yield* repository(context))), Effect.provideService(EntityTag, entity));
171
168
  return yield* HttpServerResponse.json(makeEntityEnvelope(entityConstructor, saved, links.record(saved.id)));
172
- }).pipe(Effect.catch((error)=>invalid(error, 'invalid request body', 'invalidBody')));
169
+ }).pipe(Effect.catch(legacyAnswer('invalidBody')));
173
170
  /**
174
171
  * Answers with an envelope rather than a bare `204`: every message between
175
172
  * entifix artifacts is one, and the fetch client parses every response as
@@ -187,7 +184,7 @@ const READS = new Set([
187
184
  id: params.id
188
185
  }
189
186
  });
190
- }).pipe(Effect.catch(routeFailure));
187
+ }).pipe(Effect.catch(legacyAnswer('invalid')));
191
188
  const generic = (verb, context)=>{
192
189
  switch(verb){
193
190
  case 'list':
@@ -206,11 +203,14 @@ const READS = new Set([
206
203
  * Each guard asks for different services, so the branches do not unify on
207
204
  * their own; the cast states the union every guard this set can reach
208
205
  * actually needs.
209
- */ const handlerFor = (verb)=>guarded(verb, (context)=>{
206
+ */ const handlerFor = (verb)=>{
207
+ var _options_policy;
208
+ return respond(guarded(verb, (context)=>{
210
209
  var _options_override;
211
210
  const override = (_options_override = options.override) == null ? void 0 : _options_override[verb];
212
211
  return override ? override(context, links) : generic(verb, context);
213
- });
212
+ }).pipe(Effect.catch(legacyAnswer('invalid'))), (_options_policy = options.policy) != null ? _options_policy : defaultFailurePolicy);
213
+ };
214
214
  const ROUTES = {
215
215
  list: {
216
216
  method: 'GET',
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/entity-routes.ts"],"sourcesContent":["import type { Action } from '@entifix/authz';\nimport {\n deleteUCFactory,\n EntityIdTag,\n EntityLoadRequestTag,\n EntityRepositoryFactoryTag,\n EntityRepositoryTag,\n EntityTag,\n getUCFactory,\n loadUCFactory,\n saveUCFactory,\n} from '@entifix/business';\nimport {\n EntifixBuildError,\n type EntifixEnvelopeLink,\n type Entity,\n type EntityConstructor,\n type EntityId,\n type EntityLoadRequest,\n envelopeEntityName,\n extractMetaEntity,\n makeEntityEnvelope,\n makeEntityPageEnvelope,\n parseLoadRequestParams,\n readEntityEnvelope,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';\n\nimport { entityMetadataRoute } from './entity-metadata-route.js';\nimport {\n type EntityReadScope,\n type EntityRouteContext,\n type EntityRouteGuard,\n guardRoute,\n type GuardServices,\n} from './entity-route-guard.js';\nimport {\n EntityRouteRefusal,\n invalid,\n routeFailure,\n} from './route-responses.js';\n\n/** The five generic verbs a route set can mount. */\nexport type EntityVerb = 'list' | 'get' | 'create' | 'update' | 'delete';\n\nexport {\n type EntityReadScope,\n type EntityRouteContext,\n type EntityRouteGuard,\n} from './entity-route-guard.js';\nexport { EntityRouteRefusal } from './route-responses.js';\n\n/** A hand-written handler for one verb, mounted under the derived guard. */\nexport type EntityRouteOverride<R> = (\n context: EntityRouteContext,\n links: EntityRouteLinks,\n) => Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, R>;\n\n/** The affordances a route set derives from the verbs it mounts. */\nexport interface EntityRouteLinks {\n readonly record: (id: EntityId) => EntifixEnvelopeLink[];\n readonly collection: () => EntifixEnvelopeLink[];\n}\n\nexport interface EntityRoutesOptions<\n T extends Entity,\n R,\n G extends EntityRouteGuard = EntityRouteGuard,\n> {\n readonly verbs: readonly EntityVerb[];\n /** One guard for every verb, or one for the reads and one for the writes. */\n readonly guard: G | { readonly read: G; readonly write: G };\n /** Defaults to `/api/<entity key>`. */\n readonly basePath?: `/${string}`;\n /** Narrows the reads to what this caller may see. */\n readonly scope?: (\n context: EntityRouteContext,\n ) => Effect.Effect<EntityReadScope<T>, never, R>;\n /**\n * Runs on every create and update, after the body is read and before the\n * save — and may change what is saved, or refuse it.\n */\n readonly prepare?: (\n entity: T,\n context: EntityRouteContext & { readonly verb: 'create' | 'update' },\n ) => Effect.Effect<T, EntityRouteRefusal | EntifixBuildError, R>;\n /**\n * A hand-written handler in place of the generic one for a verb. It keeps\n * the derived guard and receives the derived links, so the escape hatch\n * cannot drift from the rest of the set.\n */\n readonly override?: Partial<Record<EntityVerb, EntityRouteOverride<R>>>;\n}\n\nconst ACTION: Record<EntityVerb, Action> = {\n list: 'read',\n get: 'read',\n create: 'write',\n update: 'write',\n delete: 'delete',\n};\n\nconst READS: ReadonlySet<EntityVerb> = new Set(['list', 'get']);\n\n/**\n * The generic routes for one entity — list, read, create, update, delete and\n * its `$metadata` — derived from the entity and bound to storage through the\n * host's {@link EntityRepositoryFactoryTag}.\n *\n * What a route set varies on is stated, not written: which verbs exist, who may\n * reach them, which records a reader sees, and what a save must pass. What it\n * never varies on is the wire: the load request is parsed against the entity's\n * own metadata (so a query can only name a member the entity declared\n * filterable or sortable), every answer is an envelope, and the links name\n * exactly the verbs mounted — never one that would `404`.\n *\n * A path that is not one of these verbs is written by hand beside it, and a\n * verb whose generic form does not fit is replaced with `override`.\n */\nconst deriveEntityRoutes = <\n T extends Entity,\n R = never,\n // `never`, not the whole union: TypeScript infers no type argument once a\n // caller names one, so `makeEntityRoutes<T, R>(…)` used to default `G` to\n // every guard. Then the crossing tags became requirements of a route that\n // guards on a session, and the error surfaced at the host's `Layer.provide`\n // instead of here. Defaulting to `never` refuses that call at the guard\n // itself; `makeEntityRoutes.for<R>()` is the way to name `R` alone.\n const G extends EntityRouteGuard = never,\n>(\n entityConstructor: EntityConstructor<T>,\n options: EntityRoutesOptions<T, R, G>,\n) => {\n const key = envelopeEntityName(entityConstructor);\n const base: `/${string}` = options.basePath ?? `/api/${key}`;\n const mounted = new Set(options.verbs);\n\n const links: EntityRouteLinks = {\n record: id => {\n const href = `${base}/${String(id)}`;\n return [\n ...(mounted.has('get')\n ? [{ rel: 'self', href, method: 'GET' } as const]\n : []),\n ...(mounted.has('list')\n ? [{ rel: 'list', href: base, method: 'GET' } as const]\n : []),\n ...(mounted.has('update')\n ? [{ rel: 'update', href, method: 'PUT' } as const]\n : []),\n ...(mounted.has('delete')\n ? [{ rel: 'delete', href, method: 'DELETE' } as const]\n : []),\n ];\n },\n collection: () => [\n ...(mounted.has('list')\n ? [{ rel: 'self', href: base, method: 'GET' } as const]\n : []),\n ...(mounted.has('create')\n ? [{ rel: 'create', href: base, method: 'POST' } as const]\n : []),\n ],\n };\n\n const guardFor = (verb: EntityVerb): EntityRouteGuard => {\n const guard = options.guard as\n | EntityRouteGuard\n | { readonly read: EntityRouteGuard; readonly write: EntityRouteGuard };\n return 'kind' in guard ? guard : READS.has(verb) ? guard.read : guard.write;\n };\n\n const guarded = <A, E, RR>(\n verb: EntityVerb,\n use: (context: EntityRouteContext) => Effect.Effect<A, E, RR>,\n ) => guardRoute(entityConstructor, guardFor(verb), ACTION[verb], use);\n\n const repository = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const factory = yield* EntityRepositoryFactoryTag;\n return yield* factory.for(entityConstructor, {\n ...(context.organizationId === undefined\n ? {}\n : { organizationId: context.organizationId }),\n });\n });\n\n const scopeOf = (context: EntityRouteContext) =>\n options.scope ? options.scope(context) : Effect.succeed('all' as const);\n\n const readLoadRequest = Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const search = new URL(request.url, 'http://localhost').searchParams;\n return yield* Effect.try({\n try: () =>\n parseLoadRequestParams(\n entityConstructor,\n search,\n ) as unknown as EntityLoadRequest<T>,\n catch: error => error as EntifixBuildError,\n });\n });\n\n const notFound = HttpServerResponse.json(\n {\n message: 'not found',\n code: 'notFound',\n entity: extractMetaEntity(entityConstructor).key,\n },\n { status: 404 },\n );\n\n const list = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const parsed = yield* readLoadRequest;\n const scope = yield* scopeOf(context);\n if (scope === 'none') {\n // Still parsed: whether a query is well-formed must not depend on who\n // asked.\n return yield* HttpServerResponse.json(\n makeEntityPageEnvelope(\n entityConstructor,\n { items: [], total: 0, request: parsed },\n links.collection(),\n ),\n );\n }\n // Conjoined, never substituted: the translator `$and`s every top-level\n // element, so an added element can only narrow, and no `or` in a query\n // string can escape it.\n const request: EntityLoadRequest<T> =\n scope === 'all'\n ? parsed\n : {\n ...parsed,\n filtering: [...(parsed.filtering ?? []), scope.filter],\n };\n const page = yield* loadUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(\n EntityLoadRequestTag,\n request as unknown as EntityLoadRequest,\n ),\n );\n return yield* HttpServerResponse.json(\n makeEntityPageEnvelope(entityConstructor, page, links.collection()),\n );\n }).pipe(\n Effect.catch(error => invalid(error, 'invalid query', 'invalidQuery')),\n );\n\n const get = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const params = yield* HttpRouter.params;\n const scope = yield* scopeOf(context);\n if (scope === 'none') return yield* notFound;\n const found = yield* getUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityIdTag, params.id),\n Effect.option,\n );\n // Out of scope answers exactly as absent does: a `403` would confirm the\n // record exists to somebody who may not read it.\n if (\n found._tag === 'None' ||\n (scope !== 'all' && !scope.includes(found.value))\n ) {\n return yield* notFound;\n }\n return yield* HttpServerResponse.json(\n makeEntityEnvelope(\n entityConstructor,\n found.value,\n links.record(params.id),\n ),\n );\n }).pipe(Effect.catch(routeFailure));\n\n const save = (verb: 'create' | 'update') => (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const body = yield* request.json;\n let entity = yield* readEntityEnvelope(entityConstructor, body);\n\n // On update the URL is authoritative, so a record cannot be renamed\n // onto another one by editing its payload.\n if (verb === 'update') {\n const params = yield* HttpRouter.params;\n entity.id = params.id;\n }\n if (options.prepare) {\n entity = yield* options.prepare(entity, { ...context, verb });\n }\n\n const saved = yield* saveUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityTag, entity),\n );\n return yield* HttpServerResponse.json(\n makeEntityEnvelope(entityConstructor, saved, links.record(saved.id)),\n );\n }).pipe(\n Effect.catch(error =>\n invalid(error, 'invalid request body', 'invalidBody'),\n ),\n );\n\n /**\n * Answers with an envelope rather than a bare `204`: every message between\n * entifix artifacts is one, and the fetch client parses every response as\n * JSON.\n */\n const remove = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const params = yield* HttpRouter.params;\n yield* deleteUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityIdTag, params.id),\n );\n return yield* HttpServerResponse.json({\n meta: { type: 'entity', entity: key, links: links.collection() },\n data: { id: params.id },\n });\n }).pipe(Effect.catch(routeFailure));\n\n const generic = (verb: EntityVerb, context: EntityRouteContext) => {\n switch (verb) {\n case 'list':\n return list(context);\n case 'get':\n return get(context);\n case 'create':\n return save('create')(context);\n case 'update':\n return save('update')(context);\n case 'delete':\n return remove(context);\n }\n };\n\n /**\n * Each guard asks for different services, so the branches do not unify on\n * their own; the cast states the union every guard this set can reach\n * actually needs.\n */\n const handlerFor = (verb: EntityVerb) =>\n guarded(verb, context => {\n const override = options.override?.[verb];\n return override ? override(context, links) : generic(verb, context);\n }) as Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n unknown,\n | R\n | EntityRepositoryFactoryTag\n | GuardServices<G>\n | HttpServerRequest.HttpServerRequest\n | HttpRouter.RouteContext\n >;\n\n const ROUTES: Record<\n EntityVerb,\n { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; path: `/${string}` }\n > = {\n list: { method: 'GET', path: base },\n get: { method: 'GET', path: `${base}/:id` },\n create: { method: 'POST', path: base },\n update: { method: 'PUT', path: `${base}/:id` },\n delete: { method: 'DELETE', path: `${base}/:id` },\n };\n\n return HttpRouter.addAll([\n // A literal, so it wins over `/:id` regardless of order.\n HttpRouter.route(\n 'GET',\n `${base}/$metadata`,\n entityMetadataRoute(entityConstructor),\n ),\n ...options.verbs.map(verb =>\n HttpRouter.route(\n ROUTES[verb].method,\n ROUTES[verb].path,\n handlerFor(verb),\n ),\n ),\n ]);\n};\n\n/**\n * {@link deriveEntityRoutes}, with a curried form for a caller that has to\n * name the services its `override`, `scope` or `prepare` handlers need:\n * `makeEntityRoutes.for<R>()(Entity, options)` names `R` and still infers the\n * entity and the guard from the value, which a single call with explicit type\n * arguments cannot do.\n */\nexport { routeFailure };\n\nexport const makeEntityRoutes = Object.assign(deriveEntityRoutes, {\n for:\n <R>() =>\n <T extends Entity, const G extends EntityRouteGuard>(\n entityConstructor: EntityConstructor<T>,\n options: EntityRoutesOptions<T, R, G>,\n ) =>\n deriveEntityRoutes<T, R, G>(entityConstructor, options),\n});\n"],"names":["deleteUCFactory","EntityIdTag","EntityLoadRequestTag","EntityRepositoryFactoryTag","EntityRepositoryTag","EntityTag","getUCFactory","loadUCFactory","saveUCFactory","envelopeEntityName","extractMetaEntity","makeEntityEnvelope","makeEntityPageEnvelope","parseLoadRequestParams","readEntityEnvelope","Effect","HttpRouter","HttpServerRequest","HttpServerResponse","entityMetadataRoute","guardRoute","invalid","routeFailure","EntityRouteRefusal","ACTION","list","get","create","update","delete","READS","Set","deriveEntityRoutes","entityConstructor","options","key","base","basePath","mounted","verbs","links","record","id","href","String","has","rel","method","collection","guardFor","verb","guard","read","write","guarded","use","repository","context","gen","factory","for","organizationId","undefined","scopeOf","scope","succeed","readLoadRequest","request","search","URL","url","searchParams","try","catch","error","notFound","json","message","code","entity","status","parsed","items","total","filtering","filter","page","pipe","provideService","params","found","option","_tag","includes","value","save","body","prepare","saved","remove","meta","type","data","generic","handlerFor","override","ROUTES","path","addAll","route","map","makeEntityRoutes","Object","assign"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/entity-routes.ts"],"sourcesContent":["import type { Action } from '@entifix/authz';\nimport {\n deleteUCFactory,\n EntityIdTag,\n EntityLoadRequestTag,\n EntityRepositoryFactoryTag,\n EntityRepositoryTag,\n EntityTag,\n getUCFactory,\n loadUCFactory,\n saveUCFactory,\n} from '@entifix/business';\nimport {\n type EntifixBuildError,\n type EntifixEnvelopeLink,\n type Entity,\n type EntityConstructor,\n type EntityId,\n type EntityLoadRequest,\n envelopeEntityName,\n extractMetaEntity,\n makeEntityEnvelope,\n makeEntityPageEnvelope,\n NotFound,\n parseLoadRequestParams,\n readEntityEnvelope,\n} from '@entifix/core';\nimport { Effect } from 'effect';\nimport { HttpRouter, HttpServerRequest, HttpServerResponse } from 'effect/http';\n\nimport { entityMetadataRoute } from './entity-metadata-route.js';\nimport {\n type EntityReadScope,\n type EntityRouteContext,\n type EntityRouteGuard,\n guardRoute,\n type GuardServices,\n} from './entity-route-guard.js';\nimport {\n defaultFailurePolicy,\n type FailurePolicy,\n respond,\n} from './respond.js';\nimport {\n type EntityRouteRefusal,\n legacyAnswer,\n routeFailure,\n} from './route-responses.js';\n\n/** The five generic verbs a route set can mount. */\nexport type EntityVerb = 'list' | 'get' | 'create' | 'update' | 'delete';\n\nexport {\n type EntityReadScope,\n type EntityRouteContext,\n type EntityRouteGuard,\n} from './entity-route-guard.js';\nexport { EntityRouteRefusal } from './route-responses.js';\n\n/** A hand-written handler for one verb, mounted under the derived guard. */\nexport type EntityRouteOverride<R> = (\n context: EntityRouteContext,\n links: EntityRouteLinks,\n) => Effect.Effect<HttpServerResponse.HttpServerResponse, unknown, R>;\n\n/** The affordances a route set derives from the verbs it mounts. */\nexport interface EntityRouteLinks {\n readonly record: (id: EntityId) => EntifixEnvelopeLink[];\n readonly collection: () => EntifixEnvelopeLink[];\n}\n\nexport interface EntityRoutesOptions<\n T extends Entity,\n R,\n G extends EntityRouteGuard = EntityRouteGuard,\n> {\n readonly verbs: readonly EntityVerb[];\n /** One guard for every verb, or one for the reads and one for the writes. */\n readonly guard: G | { readonly read: G; readonly write: G };\n /** Defaults to `/api/<entity key>`. */\n readonly basePath?: `/${string}`;\n /** Narrows the reads to what this caller may see. */\n readonly scope?: (\n context: EntityRouteContext,\n ) => Effect.Effect<EntityReadScope<T>, never, R>;\n /**\n * Runs on every create and update, after the body is read and before the\n * save — and may change what is saved, or refuse it.\n */\n readonly prepare?: (\n entity: T,\n context: EntityRouteContext & { readonly verb: 'create' | 'update' },\n ) => Effect.Effect<T, EntityRouteRefusal | EntifixBuildError, R>;\n /**\n * A hand-written handler in place of the generic one for a verb. It keeps\n * the derived guard and receives the derived links, so the escape hatch\n * cannot drift from the rest of the set.\n */\n readonly override?: Partial<Record<EntityVerb, EntityRouteOverride<R>>>;\n /**\n * How every verb's failures are answered and logged. Defaults to\n * {@link defaultFailurePolicy}; a host passes its fleet policy.\n */\n readonly policy?: FailurePolicy;\n}\n\nconst ACTION: Record<EntityVerb, Action> = {\n list: 'read',\n get: 'read',\n create: 'write',\n update: 'write',\n delete: 'delete',\n};\n\nconst READS: ReadonlySet<EntityVerb> = new Set(['list', 'get']);\n\n/**\n * The generic routes for one entity — list, read, create, update, delete and\n * its `$metadata` — derived from the entity and bound to storage through the\n * host's {@link EntityRepositoryFactoryTag}.\n *\n * What a route set varies on is stated, not written: which verbs exist, who may\n * reach them, which records a reader sees, and what a save must pass. What it\n * never varies on is the wire: the load request is parsed against the entity's\n * own metadata (so a query can only name a member the entity declared\n * filterable or sortable), every answer is an envelope, and the links name\n * exactly the verbs mounted — never one that would `404`.\n *\n * A path that is not one of these verbs is written by hand beside it, and a\n * verb whose generic form does not fit is replaced with `override`.\n */\nconst deriveEntityRoutes = <\n T extends Entity,\n R = never,\n // `never`, not the whole union: TypeScript infers no type argument once a\n // caller names one, so `makeEntityRoutes<T, R>(…)` used to default `G` to\n // every guard. Then the crossing tags became requirements of a route that\n // guards on a session, and the error surfaced at the host's `Layer.provide`\n // instead of here. Defaulting to `never` refuses that call at the guard\n // itself; `makeEntityRoutes.for<R>()` is the way to name `R` alone.\n const G extends EntityRouteGuard = never,\n>(\n entityConstructor: EntityConstructor<T>,\n options: EntityRoutesOptions<T, R, G>,\n) => {\n const key = envelopeEntityName(entityConstructor);\n const base: `/${string}` = options.basePath ?? `/api/${key}`;\n const mounted = new Set(options.verbs);\n\n const links: EntityRouteLinks = {\n record: id => {\n const href = `${base}/${String(id)}`;\n return [\n ...(mounted.has('get')\n ? [{ rel: 'self', href, method: 'GET' } as const]\n : []),\n ...(mounted.has('list')\n ? [{ rel: 'list', href: base, method: 'GET' } as const]\n : []),\n ...(mounted.has('update')\n ? [{ rel: 'update', href, method: 'PUT' } as const]\n : []),\n ...(mounted.has('delete')\n ? [{ rel: 'delete', href, method: 'DELETE' } as const]\n : []),\n ];\n },\n collection: () => [\n ...(mounted.has('list')\n ? [{ rel: 'self', href: base, method: 'GET' } as const]\n : []),\n ...(mounted.has('create')\n ? [{ rel: 'create', href: base, method: 'POST' } as const]\n : []),\n ],\n };\n\n const guardFor = (verb: EntityVerb): EntityRouteGuard => {\n const guard = options.guard as\n | EntityRouteGuard\n | { readonly read: EntityRouteGuard; readonly write: EntityRouteGuard };\n return 'kind' in guard ? guard : READS.has(verb) ? guard.read : guard.write;\n };\n\n const guarded = <A, E, RR>(\n verb: EntityVerb,\n use: (context: EntityRouteContext) => Effect.Effect<A, E, RR>,\n ) => guardRoute(entityConstructor, guardFor(verb), ACTION[verb], use);\n\n const repository = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const factory = yield* EntityRepositoryFactoryTag;\n return yield* factory.for(entityConstructor, {\n ...(context.organizationId === undefined\n ? {}\n : { organizationId: context.organizationId }),\n });\n });\n\n const scopeOf = (context: EntityRouteContext) =>\n options.scope ? options.scope(context) : Effect.succeed('all' as const);\n\n const readLoadRequest = Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const search = new URL(request.url, 'http://localhost').searchParams;\n return yield* Effect.try({\n try: () =>\n parseLoadRequestParams(\n entityConstructor,\n search,\n ) as unknown as EntityLoadRequest<T>,\n catch: error => error as EntifixBuildError,\n });\n });\n\n const notFound = Effect.fail(\n new NotFound({ entity: extractMetaEntity(entityConstructor).key }),\n );\n\n const list = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const parsed = yield* readLoadRequest;\n const scope = yield* scopeOf(context);\n if (scope === 'none') {\n // Still parsed: whether a query is well-formed must not depend on who\n // asked.\n return yield* HttpServerResponse.json(\n makeEntityPageEnvelope(\n entityConstructor,\n { items: [], total: 0, request: parsed },\n links.collection(),\n ),\n );\n }\n // Conjoined, never substituted: the translator `$and`s every top-level\n // element, so an added element can only narrow, and no `or` in a query\n // string can escape it.\n const request: EntityLoadRequest<T> =\n scope === 'all'\n ? parsed\n : {\n ...parsed,\n filtering: [...(parsed.filtering ?? []), scope.filter],\n };\n const page = yield* loadUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(\n EntityLoadRequestTag,\n request as unknown as EntityLoadRequest,\n ),\n );\n return yield* HttpServerResponse.json(\n makeEntityPageEnvelope(entityConstructor, page, links.collection()),\n );\n }).pipe(Effect.catch(legacyAnswer('invalidQuery')));\n\n const get = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const params = yield* HttpRouter.params;\n const scope = yield* scopeOf(context);\n if (scope === 'none') return yield* notFound;\n const found = yield* getUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityIdTag, params.id),\n Effect.option,\n );\n // Out of scope answers exactly as absent does: a `403` would confirm the\n // record exists to somebody who may not read it.\n if (\n found._tag === 'None' ||\n (scope !== 'all' && !scope.includes(found.value))\n ) {\n return yield* notFound;\n }\n return yield* HttpServerResponse.json(\n makeEntityEnvelope(\n entityConstructor,\n found.value,\n links.record(params.id),\n ),\n );\n }).pipe(Effect.catch(legacyAnswer('invalid')));\n\n const save = (verb: 'create' | 'update') => (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const request = yield* HttpServerRequest.HttpServerRequest;\n const body = yield* request.json;\n let entity = yield* readEntityEnvelope(entityConstructor, body);\n\n // On update the URL is authoritative, so a record cannot be renamed\n // onto another one by editing its payload.\n if (verb === 'update') {\n const params = yield* HttpRouter.params;\n entity.id = params.id;\n }\n if (options.prepare) {\n entity = yield* options.prepare(entity, { ...context, verb });\n }\n\n const saved = yield* saveUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityTag, entity),\n );\n return yield* HttpServerResponse.json(\n makeEntityEnvelope(entityConstructor, saved, links.record(saved.id)),\n );\n }).pipe(Effect.catch(legacyAnswer('invalidBody')));\n\n /**\n * Answers with an envelope rather than a bare `204`: every message between\n * entifix artifacts is one, and the fetch client parses every response as\n * JSON.\n */\n const remove = (context: EntityRouteContext) =>\n Effect.gen(function* () {\n const params = yield* HttpRouter.params;\n yield* deleteUCFactory<T>().pipe(\n Effect.provideService(EntityRepositoryTag, yield* repository(context)),\n Effect.provideService(EntityIdTag, params.id),\n );\n return yield* HttpServerResponse.json({\n meta: { type: 'entity', entity: key, links: links.collection() },\n data: { id: params.id },\n });\n }).pipe(Effect.catch(legacyAnswer('invalid')));\n\n const generic = (verb: EntityVerb, context: EntityRouteContext) => {\n switch (verb) {\n case 'list':\n return list(context);\n case 'get':\n return get(context);\n case 'create':\n return save('create')(context);\n case 'update':\n return save('update')(context);\n case 'delete':\n return remove(context);\n }\n };\n\n /**\n * Each guard asks for different services, so the branches do not unify on\n * their own; the cast states the union every guard this set can reach\n * actually needs.\n */\n const handlerFor = (verb: EntityVerb) =>\n respond(\n (\n guarded(verb, context => {\n const override = options.override?.[verb];\n return override ? override(context, links) : generic(verb, context);\n }) as Effect.Effect<\n HttpServerResponse.HttpServerResponse,\n unknown,\n | R\n | EntityRepositoryFactoryTag\n | GuardServices<G>\n | HttpServerRequest.HttpServerRequest\n | HttpRouter.RouteContext\n >\n ).pipe(Effect.catch(legacyAnswer('invalid'))),\n options.policy ?? defaultFailurePolicy,\n );\n\n const ROUTES: Record<\n EntityVerb,\n { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; path: `/${string}` }\n > = {\n list: { method: 'GET', path: base },\n get: { method: 'GET', path: `${base}/:id` },\n create: { method: 'POST', path: base },\n update: { method: 'PUT', path: `${base}/:id` },\n delete: { method: 'DELETE', path: `${base}/:id` },\n };\n\n return HttpRouter.addAll([\n // A literal, so it wins over `/:id` regardless of order.\n HttpRouter.route(\n 'GET',\n `${base}/$metadata`,\n entityMetadataRoute(entityConstructor),\n ),\n ...options.verbs.map(verb =>\n HttpRouter.route(\n ROUTES[verb].method,\n ROUTES[verb].path,\n handlerFor(verb),\n ),\n ),\n ]);\n};\n\n/**\n * {@link deriveEntityRoutes}, with a curried form for a caller that has to\n * name the services its `override`, `scope` or `prepare` handlers need:\n * `makeEntityRoutes.for<R>()(Entity, options)` names `R` and still infers the\n * entity and the guard from the value, which a single call with explicit type\n * arguments cannot do.\n */\nexport { routeFailure };\n\nexport const makeEntityRoutes = Object.assign(deriveEntityRoutes, {\n for:\n <R>() =>\n <T extends Entity, const G extends EntityRouteGuard>(\n entityConstructor: EntityConstructor<T>,\n options: EntityRoutesOptions<T, R, G>,\n ) =>\n deriveEntityRoutes<T, R, G>(entityConstructor, options),\n});\n"],"names":["deleteUCFactory","EntityIdTag","EntityLoadRequestTag","EntityRepositoryFactoryTag","EntityRepositoryTag","EntityTag","getUCFactory","loadUCFactory","saveUCFactory","envelopeEntityName","extractMetaEntity","makeEntityEnvelope","makeEntityPageEnvelope","NotFound","parseLoadRequestParams","readEntityEnvelope","Effect","HttpRouter","HttpServerRequest","HttpServerResponse","entityMetadataRoute","guardRoute","defaultFailurePolicy","respond","legacyAnswer","routeFailure","EntityRouteRefusal","ACTION","list","get","create","update","delete","READS","Set","deriveEntityRoutes","entityConstructor","options","key","base","basePath","mounted","verbs","links","record","id","href","String","has","rel","method","collection","guardFor","verb","guard","read","write","guarded","use","repository","context","gen","factory","for","organizationId","undefined","scopeOf","scope","succeed","readLoadRequest","request","search","URL","url","searchParams","try","catch","error","notFound","fail","entity","parsed","json","items","total","filtering","filter","page","pipe","provideService","params","found","option","_tag","includes","value","save","body","prepare","saved","remove","meta","type","data","generic","handlerFor","override","policy","ROUTES","path","addAll","route","map","makeEntityRoutes","Object","assign"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"health-routes.d.ts","sourceRoot":"","sources":["../src/health-routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE3E,OAAO,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAe7D;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,eAAO,MAAM,YAAY,GAAI,aAAa,MAAM,4PAwD/C,CAAC"}
1
+ {"version":3,"file":"health-routes.d.ts","sourceRoot":"","sources":["../src/health-routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAE3E,OAAO,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAiB7D;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,eAAO,MAAM,YAAY,GAAI,aAAa,MAAM,4PA0D/C,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import { HealthRegistryTag, ShutdownRegistryTag } from '@entifix/business';
2
2
  import { Effect, Ref } from 'effect';
3
3
  import { HttpRouter, HttpServerResponse } from 'effect/http';
4
+ import { respond } from './respond.js';
4
5
  /**
5
6
  * How long a readiness result is reused. These endpoints are unauthenticated by
6
7
  * necessity (a probe cannot hold a session), so without a cache every request
@@ -54,7 +55,7 @@ export const healthRoutes = (serviceName)=>{
54
55
  status: 'live',
55
56
  service: serviceName
56
57
  })),
57
- HttpRouter.route('GET', '/api/health/ready', Effect.gen(function*() {
58
+ HttpRouter.route('GET', '/api/health/ready', respond(Effect.gen(function*() {
58
59
  const shutdown = yield* ShutdownRegistryTag;
59
60
  // Ahead of the cache, deliberately: a report cached a moment before the
60
61
  // signal landed would go on answering `200` for up to READY_CACHE_MS,
@@ -78,7 +79,7 @@ export const healthRoutes = (serviceName)=>{
78
79
  }, {
79
80
  status: report.ready ? 200 : 503
80
81
  });
81
- }))
82
+ })))
82
83
  ]);
83
84
  };
84
85
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/health-routes.ts"],"sourcesContent":["import type { HealthReport } from '@entifix/business';\nimport { HealthRegistryTag, ShutdownRegistryTag } from '@entifix/business';\nimport { Effect, Ref } from 'effect';\nimport { HttpRouter, HttpServerResponse } from 'effect/http';\n\n/**\n * How long a readiness result is reused. These endpoints are unauthenticated by\n * necessity (a probe cannot hold a session), so without a cache every request\n * would reach Mongo — a cheap lever for anyone who can reach the port, and\n * noise in the datastore's own metrics.\n */\nconst READY_CACHE_MS = 1_000;\n\ninterface CacheEntry {\n readonly at: number;\n readonly report: HealthReport;\n}\n\n/**\n * The probe endpoints, as a route layer to merge beside a service's own.\n *\n * - `GET /api/health` — the original endpoint, unchanged. Kept because\n * Playwright's `readyPath` and existing tooling point at it.\n * - `GET /api/health/live` — **liveness**. Answers from the process alone: no\n * datastore, no config-service, no session. This is deliberate and load\n * bearing. A liveness probe that checks dependencies turns a Mongo blip into\n * \"Kubernetes restarts every replica\", which is an outage the probe caused.\n * - `GET /api/health/ready` — **readiness**: `200` when every registered probe\n * passes, `503 {status:'degraded', failing:[…]}` otherwise. Kubernetes pulls\n * a not-ready pod out of the load balancer without killing it, which is what\n * a reconnecting client wants. A process that has begun terminating answers\n * `503 {status:'terminating'}` instead, **before** any probe runs: nothing is\n * failing, and running the probes would ask a closing pool a question whose\n * answer changes nothing.\n *\n * The failing list carries probe **names** only — never a URI, host, or driver\n * message. This endpoint answers anyone who can reach the port.\n */\n// The return type is inferred rather than annotated: `HttpServerResponse.json`\n// widens the routes' error channel with `HttpBodyError`, and spelling that out\n// here would only duplicate what the platform types already say.\nexport const healthRoutes = (serviceName: string) => {\n // One cache per server instance (the e2e mock profile boots services\n // in-process, so a module-level cache would leak across them).\n const cache = Ref.makeUnsafe<CacheEntry | null>(null);\n\n const cachedReport = Effect.gen(function* () {\n const now = Date.now();\n const entry = yield* Ref.get(cache);\n if (entry !== null && now - entry.at < READY_CACHE_MS) return entry.report;\n\n const registry = yield* HealthRegistryTag;\n const report = yield* registry.report;\n yield* Ref.set(cache, { at: now, report });\n return report;\n });\n\n return HttpRouter.addAll([\n HttpRouter.route(\n 'GET',\n '/api/health',\n HttpServerResponse.json({ status: 'ok', service: serviceName }),\n ),\n HttpRouter.route(\n 'GET',\n '/api/health/live',\n HttpServerResponse.json({ status: 'live', service: serviceName }),\n ),\n HttpRouter.route(\n 'GET',\n '/api/health/ready',\n Effect.gen(function* () {\n const shutdown = yield* ShutdownRegistryTag;\n // Ahead of the cache, deliberately: a report cached a moment before the\n // signal landed would go on answering `200` for up to READY_CACHE_MS,\n // which is precisely the window a rolling restart lives in.\n if (yield* shutdown.terminating) {\n return yield* HttpServerResponse.json(\n { status: 'terminating', service: serviceName },\n { status: 503 },\n );\n }\n\n const report = yield* cachedReport;\n return yield* HttpServerResponse.json(\n report.ready\n ? { status: 'ready', service: serviceName }\n : {\n status: 'degraded',\n service: serviceName,\n failing: report.failing,\n },\n { status: report.ready ? 200 : 503 },\n );\n }),\n ),\n ]);\n};\n"],"names":["HealthRegistryTag","ShutdownRegistryTag","Effect","Ref","HttpRouter","HttpServerResponse","READY_CACHE_MS","healthRoutes","serviceName","cache","makeUnsafe","cachedReport","gen","now","Date","entry","get","at","report","registry","set","addAll","route","json","status","service","shutdown","terminating","ready","failing"],"mappings":"AACA,SAASA,iBAAiB,EAAEC,mBAAmB,QAAQ,oBAAoB;AAC3E,SAASC,MAAM,EAAEC,GAAG,QAAQ,SAAS;AACrC,SAASC,UAAU,EAAEC,kBAAkB,QAAQ,cAAc;AAE7D;;;;;CAKC,GACD,MAAMC,iBAAiB;AAOvB;;;;;;;;;;;;;;;;;;;CAmBC,GACD,+EAA+E;AAC/E,+EAA+E;AAC/E,iEAAiE;AACjE,OAAO,MAAMC,eAAe,CAACC;IAC3B,qEAAqE;IACrE,+DAA+D;IAC/D,MAAMC,QAAQN,IAAIO,UAAU,CAAoB;IAEhD,MAAMC,eAAeT,OAAOU,GAAG,CAAC;QAC9B,MAAMC,MAAMC,KAAKD,GAAG;QACpB,MAAME,QAAQ,OAAOZ,IAAIa,GAAG,CAACP;QAC7B,IAAIM,UAAU,QAAQF,MAAME,MAAME,EAAE,GAAGX,gBAAgB,OAAOS,MAAMG,MAAM;QAE1E,MAAMC,WAAW,OAAOnB;QACxB,MAAMkB,SAAS,OAAOC,SAASD,MAAM;QACrC,OAAOf,IAAIiB,GAAG,CAACX,OAAO;YAAEQ,IAAIJ;YAAKK;QAAO;QACxC,OAAOA;IACT;IAEA,OAAOd,WAAWiB,MAAM,CAAC;QACvBjB,WAAWkB,KAAK,CACd,OACA,eACAjB,mBAAmBkB,IAAI,CAAC;YAAEC,QAAQ;YAAMC,SAASjB;QAAY;QAE/DJ,WAAWkB,KAAK,CACd,OACA,oBACAjB,mBAAmBkB,IAAI,CAAC;YAAEC,QAAQ;YAAQC,SAASjB;QAAY;QAEjEJ,WAAWkB,KAAK,CACd,OACA,qBACApB,OAAOU,GAAG,CAAC;YACT,MAAMc,WAAW,OAAOzB;YACxB,wEAAwE;YACxE,sEAAsE;YACtE,4DAA4D;YAC5D,IAAI,OAAOyB,SAASC,WAAW,EAAE;gBAC/B,OAAO,OAAOtB,mBAAmBkB,IAAI,CACnC;oBAAEC,QAAQ;oBAAeC,SAASjB;gBAAY,GAC9C;oBAAEgB,QAAQ;gBAAI;YAElB;YAEA,MAAMN,SAAS,OAAOP;YACtB,OAAO,OAAON,mBAAmBkB,IAAI,CACnCL,OAAOU,KAAK,GACR;gBAAEJ,QAAQ;gBAASC,SAASjB;YAAY,IACxC;gBACEgB,QAAQ;gBACRC,SAASjB;gBACTqB,SAASX,OAAOW,OAAO;YACzB,GACJ;gBAAEL,QAAQN,OAAOU,KAAK,GAAG,MAAM;YAAI;QAEvC;KAEH;AACH,EAAE"}
1
+ {"version":3,"sources":["../src/health-routes.ts"],"sourcesContent":["import type { HealthReport } from '@entifix/business';\nimport { HealthRegistryTag, ShutdownRegistryTag } from '@entifix/business';\nimport { Effect, Ref } from 'effect';\nimport { HttpRouter, HttpServerResponse } from 'effect/http';\n\nimport { respond } from './respond.js';\n\n/**\n * How long a readiness result is reused. These endpoints are unauthenticated by\n * necessity (a probe cannot hold a session), so without a cache every request\n * would reach Mongo — a cheap lever for anyone who can reach the port, and\n * noise in the datastore's own metrics.\n */\nconst READY_CACHE_MS = 1_000;\n\ninterface CacheEntry {\n readonly at: number;\n readonly report: HealthReport;\n}\n\n/**\n * The probe endpoints, as a route layer to merge beside a service's own.\n *\n * - `GET /api/health` — the original endpoint, unchanged. Kept because\n * Playwright's `readyPath` and existing tooling point at it.\n * - `GET /api/health/live` — **liveness**. Answers from the process alone: no\n * datastore, no config-service, no session. This is deliberate and load\n * bearing. A liveness probe that checks dependencies turns a Mongo blip into\n * \"Kubernetes restarts every replica\", which is an outage the probe caused.\n * - `GET /api/health/ready` — **readiness**: `200` when every registered probe\n * passes, `503 {status:'degraded', failing:[…]}` otherwise. Kubernetes pulls\n * a not-ready pod out of the load balancer without killing it, which is what\n * a reconnecting client wants. A process that has begun terminating answers\n * `503 {status:'terminating'}` instead, **before** any probe runs: nothing is\n * failing, and running the probes would ask a closing pool a question whose\n * answer changes nothing.\n *\n * The failing list carries probe **names** only — never a URI, host, or driver\n * message. This endpoint answers anyone who can reach the port.\n */\n// The return type is inferred rather than annotated: `HttpServerResponse.json`\n// widens the routes' error channel with `HttpBodyError`, and spelling that out\n// here would only duplicate what the platform types already say.\nexport const healthRoutes = (serviceName: string) => {\n // One cache per server instance (the e2e mock profile boots services\n // in-process, so a module-level cache would leak across them).\n const cache = Ref.makeUnsafe<CacheEntry | null>(null);\n\n const cachedReport = Effect.gen(function* () {\n const now = Date.now();\n const entry = yield* Ref.get(cache);\n if (entry !== null && now - entry.at < READY_CACHE_MS) return entry.report;\n\n const registry = yield* HealthRegistryTag;\n const report = yield* registry.report;\n yield* Ref.set(cache, { at: now, report });\n return report;\n });\n\n return HttpRouter.addAll([\n HttpRouter.route(\n 'GET',\n '/api/health',\n HttpServerResponse.json({ status: 'ok', service: serviceName }),\n ),\n HttpRouter.route(\n 'GET',\n '/api/health/live',\n HttpServerResponse.json({ status: 'live', service: serviceName }),\n ),\n HttpRouter.route(\n 'GET',\n '/api/health/ready',\n respond(\n Effect.gen(function* () {\n const shutdown = yield* ShutdownRegistryTag;\n // Ahead of the cache, deliberately: a report cached a moment before the\n // signal landed would go on answering `200` for up to READY_CACHE_MS,\n // which is precisely the window a rolling restart lives in.\n if (yield* shutdown.terminating) {\n return yield* HttpServerResponse.json(\n { status: 'terminating', service: serviceName },\n { status: 503 },\n );\n }\n\n const report = yield* cachedReport;\n return yield* HttpServerResponse.json(\n report.ready\n ? { status: 'ready', service: serviceName }\n : {\n status: 'degraded',\n service: serviceName,\n failing: report.failing,\n },\n { status: report.ready ? 200 : 503 },\n );\n }),\n ),\n ),\n ]);\n};\n"],"names":["HealthRegistryTag","ShutdownRegistryTag","Effect","Ref","HttpRouter","HttpServerResponse","respond","READY_CACHE_MS","healthRoutes","serviceName","cache","makeUnsafe","cachedReport","gen","now","Date","entry","get","at","report","registry","set","addAll","route","json","status","service","shutdown","terminating","ready","failing"],"mappings":"AACA,SAASA,iBAAiB,EAAEC,mBAAmB,QAAQ,oBAAoB;AAC3E,SAASC,MAAM,EAAEC,GAAG,QAAQ,SAAS;AACrC,SAASC,UAAU,EAAEC,kBAAkB,QAAQ,cAAc;AAE7D,SAASC,OAAO,QAAQ,eAAe;AAEvC;;;;;CAKC,GACD,MAAMC,iBAAiB;AAOvB;;;;;;;;;;;;;;;;;;;CAmBC,GACD,+EAA+E;AAC/E,+EAA+E;AAC/E,iEAAiE;AACjE,OAAO,MAAMC,eAAe,CAACC;IAC3B,qEAAqE;IACrE,+DAA+D;IAC/D,MAAMC,QAAQP,IAAIQ,UAAU,CAAoB;IAEhD,MAAMC,eAAeV,OAAOW,GAAG,CAAC;QAC9B,MAAMC,MAAMC,KAAKD,GAAG;QACpB,MAAME,QAAQ,OAAOb,IAAIc,GAAG,CAACP;QAC7B,IAAIM,UAAU,QAAQF,MAAME,MAAME,EAAE,GAAGX,gBAAgB,OAAOS,MAAMG,MAAM;QAE1E,MAAMC,WAAW,OAAOpB;QACxB,MAAMmB,SAAS,OAAOC,SAASD,MAAM;QACrC,OAAOhB,IAAIkB,GAAG,CAACX,OAAO;YAAEQ,IAAIJ;YAAKK;QAAO;QACxC,OAAOA;IACT;IAEA,OAAOf,WAAWkB,MAAM,CAAC;QACvBlB,WAAWmB,KAAK,CACd,OACA,eACAlB,mBAAmBmB,IAAI,CAAC;YAAEC,QAAQ;YAAMC,SAASjB;QAAY;QAE/DL,WAAWmB,KAAK,CACd,OACA,oBACAlB,mBAAmBmB,IAAI,CAAC;YAAEC,QAAQ;YAAQC,SAASjB;QAAY;QAEjEL,WAAWmB,KAAK,CACd,OACA,qBACAjB,QACEJ,OAAOW,GAAG,CAAC;YACT,MAAMc,WAAW,OAAO1B;YACxB,wEAAwE;YACxE,sEAAsE;YACtE,4DAA4D;YAC5D,IAAI,OAAO0B,SAASC,WAAW,EAAE;gBAC/B,OAAO,OAAOvB,mBAAmBmB,IAAI,CACnC;oBAAEC,QAAQ;oBAAeC,SAASjB;gBAAY,GAC9C;oBAAEgB,QAAQ;gBAAI;YAElB;YAEA,MAAMN,SAAS,OAAOP;YACtB,OAAO,OAAOP,mBAAmBmB,IAAI,CACnCL,OAAOU,KAAK,GACR;gBAAEJ,QAAQ;gBAASC,SAASjB;YAAY,IACxC;gBACEgB,QAAQ;gBACRC,SAASjB;gBACTqB,SAASX,OAAOW,OAAO;YACzB,GACJ;gBAAEL,QAAQN,OAAOU,KAAK,GAAG,MAAM;YAAI;QAEvC;KAGL;AACH,EAAE"}
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from './entity-metadata-route.js';
4
4
  export * from './entity-routes.js';
5
5
  export * from './health-routes.js';
6
6
  export * from './redact-configuration.js';
7
+ export * from './respond.js';
7
8
  export * from './service-description-route.js';
8
9
  export * from './sse.js';
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,cAAc,CAAC;AAC7B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,UAAU,CAAC"}
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export * from './entity-metadata-route.js';
4
4
  export * from './entity-routes.js';
5
5
  export * from './health-routes.js';
6
6
  export * from './redact-configuration.js';
7
+ export * from './respond.js';
7
8
  export * from './service-description-route.js';
8
9
  export * from './sse.js';
9
10
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './auth/index.js';\nexport * from './entity-facet-routes.js';\nexport * from './entity-metadata-route.js';\nexport * from './entity-routes.js';\nexport * from './health-routes.js';\nexport * from './redact-configuration.js';\nexport * from './service-description-route.js';\nexport * from './sse.js';\n"],"names":[],"mappings":"AAAA,cAAc,kBAAkB;AAChC,cAAc,2BAA2B;AACzC,cAAc,6BAA6B;AAC3C,cAAc,qBAAqB;AACnC,cAAc,qBAAqB;AACnC,cAAc,4BAA4B;AAC1C,cAAc,iCAAiC;AAC/C,cAAc,WAAW"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './auth/index.js';\nexport * from './entity-facet-routes.js';\nexport * from './entity-metadata-route.js';\nexport * from './entity-routes.js';\nexport * from './health-routes.js';\nexport * from './redact-configuration.js';\nexport * from './respond.js';\nexport * from './service-description-route.js';\nexport * from './sse.js';\n"],"names":[],"mappings":"AAAA,cAAc,kBAAkB;AAChC,cAAc,2BAA2B;AACzC,cAAc,6BAA6B;AAC3C,cAAc,qBAAqB;AACnC,cAAc,qBAAqB;AACnC,cAAc,4BAA4B;AAC1C,cAAc,eAAe;AAC7B,cAAc,iCAAiC;AAC/C,cAAc,WAAW"}
@@ -1 +1 @@
1
- {"version":3,"file":"make-service.d.ts","sourceRoot":"","sources":["../src/make-service.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,iBAAiB,EAEjB,KAAK,gBAAgB,EACrB,mBAAmB,EAEnB,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAI1D;;;;GAIG;AAEH,MAAM,MAAM,aAAa,CAAC,CAAC,GAAG,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AAElE;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAC5B,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,GACtC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;AAElE,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAC3B,iBAAiB,GAAG,mBAAmB,GAAG,iBAAiB,CAAC;AAE9D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAiB,CAAC,OAAO;IACxC,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACxC;;;;;;;;;OASG;IACH,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAC5B,kBAAkB,CAAC,OAAO,CAAC,EAC3B,KAAK,EACL,iBAAiB,CAClB,CAAC;CACH;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,EACrC,KAAK,iBAAiB,CAAC,OAAO,CAAC,EAC/B,OAAM,MAAiB,EACvB,WAAU,gBAAkD,KAgD3C,KAAK,CAAC,KAAK,CAC1B,KAAK,CAAC,OAAO,4iFAAe,EAC5B,KAAK,CAAC,KAAK,4iFAAe,CAE7B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,EAAE,KAAK,iBAAiB,CAAC,OAAO,CAAC,KAAG,IAQtE,CAAC"}
1
+ {"version":3,"file":"make-service.d.ts","sourceRoot":"","sources":["../src/make-service.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,iBAAiB,EAEjB,KAAK,gBAAgB,EACrB,mBAAmB,EAEnB,iBAAiB,EAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAK1D;;;;GAIG;AAEH,MAAM,MAAM,aAAa,CAAC,CAAC,GAAG,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AAElE;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAC5B,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,GACtC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;AAElE,2EAA2E;AAC3E,MAAM,MAAM,iBAAiB,GAC3B,iBAAiB,GAAG,mBAAmB,GAAG,iBAAiB,CAAC;AAE9D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAiB,CAAC,OAAO;IACxC,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACxC;;;;;;;;;OASG;IACH,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAC5B,kBAAkB,CAAC,OAAO,CAAC,EAC3B,KAAK,EACL,iBAAiB,CAClB,CAAC;CACH;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,EACrC,KAAK,iBAAiB,CAAC,OAAO,CAAC,EAC/B,OAAM,MAAiB,EACvB,WAAU,gBAAkD,KAkD3C,KAAK,CAAC,KAAK,CAC1B,KAAK,CAAC,OAAO,4iFAAe,EAC5B,KAAK,CAAC,KAAK,4iFAAe,CAE7B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,EAAE,KAAK,iBAAiB,CAAC,OAAO,CAAC,KAAG,IAQtE,CAAC"}
@@ -4,6 +4,7 @@ import { HealthRegistryLayer, makeShutdownRegistry, ShutdownRegistryTag, WiringR
4
4
  import { Effect, Layer } from 'effect';
5
5
  import { HttpRouter } from 'effect/http';
6
6
  import { healthRoutes } from './health-routes.js';
7
+ import { logMechanismOutcome, problemNet } from './respond.js';
7
8
  import { serviceDescriptionRoute } from './service-description-route.js';
8
9
  /**
9
10
  * The service as a `Layer`: `/api/health` mounted, `router` served over an
@@ -23,7 +24,8 @@ import { serviceDescriptionRoute } from './service-description-route.js';
23
24
  */ export const makeServerLayer = (def, port = def.port, shutdown = makeShutdownRegistry().registry)=>{
24
25
  const routes = Layer.mergeAll(def.router, healthRoutes(def.name), def.description === undefined ? Layer.empty : serviceDescriptionRoute(def.name, def.slices, def.description), // Permissive CORS (frontends call these services cross-origin from their
25
26
  // dev ports). Tighten per-environment later.
26
- HttpRouter.cors());
27
+ HttpRouter.cors(), // The last net: a defect no route answered is still a problem body.
28
+ problemNet);
27
29
  // `provideMerge` (not `provide`) so the registries the client layers
28
30
  // registered into are the same instances the readiness and description routes
29
31
  // read.
@@ -61,7 +63,7 @@ import { serviceDescriptionRoute } from './service-description-route.js';
61
63
  * be: Kubernetes runs it before it sends SIGTERM, and this repo declares no
62
64
  * Deployment for a service. It lands with the first one.
63
65
  */ export const makeService = (def)=>{
64
- const shutdown = makeShutdownRegistry();
66
+ const shutdown = makeShutdownRegistry(logMechanismOutcome);
65
67
  process.once('SIGTERM', shutdown.begin);
66
68
  process.once('SIGINT', shutdown.begin);
67
69
  NodeRuntime.runMain(Layer.launch(makeServerLayer(def, def.port, shutdown.registry)));