@geekmidas/constructs 10.0.0-alpha.69 → 10.0.0-alpha.71

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 (123) hide show
  1. package/dist/{Construct-BO1X-kt9.mjs → Construct-B1vGBPme.mjs} +2 -2
  2. package/dist/{Construct-BO1X-kt9.mjs.map → Construct-B1vGBPme.mjs.map} +1 -1
  3. package/dist/{Construct-DmvXceVY.cjs → Construct-DUSXuTHP.cjs} +2 -2
  4. package/dist/{Construct-DmvXceVY.cjs.map → Construct-DUSXuTHP.cjs.map} +1 -1
  5. package/dist/{CronBuilder-BoHzlWU_.cjs → CronBuilder-C0Ek1-RA.cjs} +3 -3
  6. package/dist/{CronBuilder-BoHzlWU_.cjs.map → CronBuilder-C0Ek1-RA.cjs.map} +1 -1
  7. package/dist/{CronBuilder-CCgt0wCE.mjs → CronBuilder-Dmix_Pen.mjs} +3 -3
  8. package/dist/{CronBuilder-CCgt0wCE.mjs.map → CronBuilder-Dmix_Pen.mjs.map} +1 -1
  9. package/dist/{Endpoint-DfiXTm_v.cjs → Endpoint-Ba1PiyuJ.cjs} +3 -3
  10. package/dist/{Endpoint-DfiXTm_v.cjs.map → Endpoint-Ba1PiyuJ.cjs.map} +1 -1
  11. package/dist/{Endpoint-DV3jK5sP.d.cts → Endpoint-DJZkADYh.d.cts} +2 -2
  12. package/dist/{Endpoint-BJDBZBZu.d.mts.map → Endpoint-DJZkADYh.d.cts.map} +1 -1
  13. package/dist/{Endpoint-CMWWrQ7u.mjs → Endpoint-ZJhv68qq.mjs} +3 -3
  14. package/dist/{Endpoint-CMWWrQ7u.mjs.map → Endpoint-ZJhv68qq.mjs.map} +1 -1
  15. package/dist/{Endpoint-BJDBZBZu.d.mts → Endpoint-hcHSFhda.d.mts} +3 -3
  16. package/dist/{Endpoint-DV3jK5sP.d.cts.map → Endpoint-hcHSFhda.d.mts.map} +1 -1
  17. package/dist/{EndpointFactory-gaF4yRpP.d.mts → EndpointFactory-B5siFRcO.d.mts} +3 -3
  18. package/dist/{EndpointFactory-gaF4yRpP.d.mts.map → EndpointFactory-B5siFRcO.d.mts.map} +1 -1
  19. package/dist/{EndpointFactory-CLxrcS0O.cjs → EndpointFactory-B_x2fXWQ.cjs} +4 -4
  20. package/dist/{EndpointFactory-CLxrcS0O.cjs.map → EndpointFactory-B_x2fXWQ.cjs.map} +1 -1
  21. package/dist/{EndpointFactory-CLzCLkLt.d.cts → EndpointFactory-BkYuVg8W.d.cts} +2 -2
  22. package/dist/{EndpointFactory-CLzCLkLt.d.cts.map → EndpointFactory-BkYuVg8W.d.cts.map} +1 -1
  23. package/dist/{EndpointFactory-7g4Dg6cW.mjs → EndpointFactory-C3QvXMk6.mjs} +4 -4
  24. package/dist/{EndpointFactory-7g4Dg6cW.mjs.map → EndpointFactory-C3QvXMk6.mjs.map} +1 -1
  25. package/dist/{HonoEndpointAdaptor-B22boEAR.cjs → HonoEndpointAdaptor-0yLvYunI.cjs} +40 -16
  26. package/dist/HonoEndpointAdaptor-0yLvYunI.cjs.map +1 -0
  27. package/dist/{HonoEndpointAdaptor-vY4JKG8i.mjs → HonoEndpointAdaptor-chgcjPmg.mjs} +40 -16
  28. package/dist/HonoEndpointAdaptor-chgcjPmg.mjs.map +1 -0
  29. package/dist/{Queue-hwPN-t31.mjs → Queue-CLRT_j5a.mjs} +2 -2
  30. package/dist/{Queue-hwPN-t31.mjs.map → Queue-CLRT_j5a.mjs.map} +1 -1
  31. package/dist/{Queue-D5jcw0mu.cjs → Queue-umpX3683.cjs} +2 -2
  32. package/dist/{Queue-D5jcw0mu.cjs.map → Queue-umpX3683.cjs.map} +1 -1
  33. package/dist/{QueueBuilder-DCuYdjMw.mjs → QueueBuilder-B0mCqq74.mjs} +2 -2
  34. package/dist/{QueueBuilder-DCuYdjMw.mjs.map → QueueBuilder-B0mCqq74.mjs.map} +1 -1
  35. package/dist/{QueueBuilder-BFQn3OzZ.cjs → QueueBuilder-B9ClmXjX.cjs} +2 -2
  36. package/dist/{QueueBuilder-BFQn3OzZ.cjs.map → QueueBuilder-B9ClmXjX.cjs.map} +1 -1
  37. package/dist/{SubscriberBuilder-DMFk_Ap1.mjs → SubscriberBuilder-CDy4nHCz.mjs} +2 -2
  38. package/dist/{SubscriberBuilder-DMFk_Ap1.mjs.map → SubscriberBuilder-CDy4nHCz.mjs.map} +1 -1
  39. package/dist/{SubscriberBuilder-C2PwqwIl.cjs → SubscriberBuilder-D_owVoHo.cjs} +2 -2
  40. package/dist/{SubscriberBuilder-C2PwqwIl.cjs.map → SubscriberBuilder-D_owVoHo.cjs.map} +1 -1
  41. package/dist/{Topic-CkxOTrMv.cjs → Topic-BliAKi5r.cjs} +2 -2
  42. package/dist/{Topic-CkxOTrMv.cjs.map → Topic-BliAKi5r.cjs.map} +1 -1
  43. package/dist/{Topic-hPffOsot.mjs → Topic-DCxIstf9.mjs} +2 -2
  44. package/dist/{Topic-hPffOsot.mjs.map → Topic-DCxIstf9.mjs.map} +1 -1
  45. package/dist/adaptors/aws.cjs +8 -8
  46. package/dist/adaptors/aws.cjs.map +1 -1
  47. package/dist/adaptors/aws.d.cts +1 -1
  48. package/dist/adaptors/aws.d.mts +2 -2
  49. package/dist/adaptors/aws.mjs +9 -9
  50. package/dist/adaptors/aws.mjs.map +1 -1
  51. package/dist/adaptors/hono.cjs +1 -1
  52. package/dist/adaptors/hono.d.cts +1 -1
  53. package/dist/adaptors/hono.d.cts.map +1 -1
  54. package/dist/adaptors/hono.d.mts +1 -1
  55. package/dist/adaptors/hono.d.mts.map +1 -1
  56. package/dist/adaptors/hono.mjs +1 -1
  57. package/dist/adaptors/testing.cjs +7 -7
  58. package/dist/adaptors/testing.d.cts +1 -1
  59. package/dist/adaptors/testing.d.mts +1 -1
  60. package/dist/adaptors/testing.mjs +7 -7
  61. package/dist/{auth-CIFXNuDk.d.cts → auth-CuCozjsh.d.cts} +62 -7
  62. package/dist/auth-CuCozjsh.d.cts.map +1 -0
  63. package/dist/{auth-CJ9Ff6Lu.d.mts → auth-XtDcvQxV.d.mts} +63 -8
  64. package/dist/auth-XtDcvQxV.d.mts.map +1 -0
  65. package/dist/auth.cjs +78 -11
  66. package/dist/auth.cjs.map +1 -1
  67. package/dist/auth.d.cts +2 -2
  68. package/dist/auth.d.mts +2 -2
  69. package/dist/auth.mjs +77 -11
  70. package/dist/auth.mjs.map +1 -1
  71. package/dist/crons/index.cjs +2 -2
  72. package/dist/crons/index.mjs +2 -2
  73. package/dist/database/kysely.cjs +1 -1
  74. package/dist/database/kysely.mjs +1 -1
  75. package/dist/endpoints/index.cjs +3 -3
  76. package/dist/endpoints/index.d.cts +2 -2
  77. package/dist/endpoints/index.d.mts +2 -2
  78. package/dist/endpoints/index.mjs +3 -3
  79. package/dist/functions/index.cjs +1 -1
  80. package/dist/functions/index.mjs +1 -1
  81. package/dist/{functions-Di6n5ltG.mjs → functions-BFizikJU.mjs} +2 -2
  82. package/dist/{functions-Di6n5ltG.mjs.map → functions-BFizikJU.mjs.map} +1 -1
  83. package/dist/{functions-CbP5ZKma.cjs → functions-CfzJG9hK.cjs} +2 -2
  84. package/dist/{functions-CbP5ZKma.cjs.map → functions-CfzJG9hK.cjs.map} +1 -1
  85. package/dist/index.cjs +1 -1
  86. package/dist/index.mjs +1 -1
  87. package/dist/{kysely-BRzzawr2.cjs → kysely-B-xWOGmn.cjs} +2 -2
  88. package/dist/kysely-B-xWOGmn.cjs.map +1 -0
  89. package/dist/{kysely-BYfM6Zby.mjs → kysely-BhJ3lLdE.mjs} +2 -2
  90. package/dist/kysely-BhJ3lLdE.mjs.map +1 -0
  91. package/dist/{processAudits-BpRlPQMW.mjs → processAudits-BRRVVHUB.mjs} +57 -3
  92. package/dist/processAudits-BRRVVHUB.mjs.map +1 -0
  93. package/dist/{processAudits-D_2W5x8k.cjs → processAudits-DasOZjPl.cjs} +69 -3
  94. package/dist/processAudits-DasOZjPl.cjs.map +1 -0
  95. package/dist/queue/index.cjs +2 -2
  96. package/dist/queue/index.mjs +2 -2
  97. package/dist/rest-api.cjs +1 -1
  98. package/dist/rest-api.d.cts +2 -2
  99. package/dist/rest-api.d.mts +2 -2
  100. package/dist/rest-api.mjs +1 -1
  101. package/dist/{runSubscriber-BYEY_bfA.mjs → runSubscriber-BHvJ8POr.mjs} +2 -2
  102. package/dist/runSubscriber-BHvJ8POr.mjs.map +1 -0
  103. package/dist/{runSubscriber-BRp9a_qi.cjs → runSubscriber-D8nLXIv1.cjs} +2 -2
  104. package/dist/runSubscriber-D8nLXIv1.cjs.map +1 -0
  105. package/dist/subscribers/index.cjs +3 -3
  106. package/dist/subscribers/index.cjs.map +1 -1
  107. package/dist/subscribers/index.mjs +3 -3
  108. package/dist/subscribers/index.mjs.map +1 -1
  109. package/dist/topic/index.cjs +1 -1
  110. package/dist/topic/index.mjs +1 -1
  111. package/dist/worker.cjs +4 -4
  112. package/dist/worker.mjs +4 -4
  113. package/package.json +31 -31
  114. package/dist/HonoEndpointAdaptor-B22boEAR.cjs.map +0 -1
  115. package/dist/HonoEndpointAdaptor-vY4JKG8i.mjs.map +0 -1
  116. package/dist/auth-CIFXNuDk.d.cts.map +0 -1
  117. package/dist/auth-CJ9Ff6Lu.d.mts.map +0 -1
  118. package/dist/kysely-BRzzawr2.cjs.map +0 -1
  119. package/dist/kysely-BYfM6Zby.mjs.map +0 -1
  120. package/dist/processAudits-BpRlPQMW.mjs.map +0 -1
  121. package/dist/processAudits-D_2W5x8k.cjs.map +0 -1
  122. package/dist/runSubscriber-BRp9a_qi.cjs.map +0 -1
  123. package/dist/runSubscriber-BYEY_bfA.mjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_Construct = require("./Construct-DmvXceVY.cjs");
2
+ const require_Construct = require("./Construct-DUSXuTHP.cjs");
3
3
  const require_shutdown = require("./shutdown-BMJmAhSt.cjs");
4
4
  exports.Construct = require_Construct.Construct;
5
5
  exports.ConstructType = require_Construct.ConstructType;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { i as snifferContext, n as ConstructType, r as sniffService, t as Construct } from "./Construct-BO1X-kt9.mjs";
1
+ import { i as snifferContext, n as ConstructType, r as sniffService, t as Construct } from "./Construct-B1vGBPme.mjs";
2
2
  import { n as runShutdownHooks, t as onShutdown } from "./shutdown-C7xa8x9S.mjs";
3
3
  export { Construct, ConstructType, onShutdown, runShutdownHooks, sniffService, snifferContext };
@@ -51,7 +51,7 @@ function openPool(url, options = {}) {
51
51
  });
52
52
  pool.on("error", (error) => {
53
53
  const logger = (0, _geekmidas_services.currentRequestContext)()?.logger;
54
- if (logger) logger.warn({ error }, "Idle database connection lost");
54
+ if (logger) logger.warn({ err: error }, "Idle database connection lost");
55
55
  else console.warn("Idle database connection lost:", error.message);
56
56
  });
57
57
  if (options.queryTags !== false) pool.on("connect", (client) => tagQueries(client));
@@ -298,4 +298,4 @@ Object.defineProperty(exports, "closeDatabasePools", {
298
298
  }
299
299
  });
300
300
 
301
- //# sourceMappingURL=kysely-BRzzawr2.cjs.map
301
+ //# sourceMappingURL=kysely-B-xWOGmn.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kysely-B-xWOGmn.cjs","names":["onShutdown","currentRequestContext","canonicalId","provideKey","DEFAULT_POSTGRES_VERSION","serviceKey","Cache","kysely","Kysely","PostgresDialect"],"sources":["../src/database/pool.ts","../src/database/kysely.ts"],"sourcesContent":["/**\n * The `pg` pools a database construct opens, and what every one of them gets:\n *\n * - **A name.** `application_name` is what `pg_stat_activity` shows beside a\n * connection. Without one, a database at `max_connections` cannot say who\n * holds them. It is a *fallback*, so `PGAPPNAME` or `?application_name=` in\n * the URL still win.\n * - **Tagged queries.** Inside a request, each query ends in a comment naming\n * what ran it — `/*operation='POST /orders',request_id='…'*\\/` — in the\n * sqlcommenter form. It shows in `pg_stat_activity.query`, in the server's\n * slow-query and `auto_explain` logs, and costs no round trip;\n * `pg_stat_statements` ignores comments, so its grouping is unchanged.\n * - **A listener for idle-client errors.** When the server ends an idle\n * connection (`idle_session_timeout`, a failover, an admin), the pool emits\n * `'error'`. With no listener that is an uncaught exception that kills the\n * process — for a client the pool has already discarded.\n * - **A way to close.** {@link closeDatabasePools} ends every pool opened, so a\n * server shutting down releases its connections instead of holding them\n * until the process dies.\n */\n\nimport { currentRequestContext } from '@geekmidas/services';\nimport pg from 'pg';\nimport { onShutdown } from '../shutdown';\n\nexport interface DatabasePoolOptions {\n\t/** Tag queries run inside a request with what ran them. Default true. */\n\tqueryTags?: boolean;\n}\n\n/** Every pool opened and not yet closed, so a shutdown can end them all. */\nconst open = new Set<pg.Pool>();\nonShutdown(() => closeDatabasePools());\n\n/**\n * A pool for one URL, with its schema actually on the search path.\n *\n * `?search_path=` is not a libpq parameter — a URL carrying it connects\n * happily and then resolves every unqualified name against `public`, which is\n * how a schema tenant silently writes its tables into the database it was\n * separated from. Postgres takes it as a startup option instead, so the URL\n * keeps the readable form the target derives and this turns it into the thing\n * the server understands.\n */\nexport function openPool(\n\turl: string,\n\toptions: DatabasePoolOptions = {},\n): pg.Pool {\n\tconst parsed = new URL(url);\n\tconst searchPath = parsed.searchParams.get('search_path');\n\tparsed.searchParams.delete('search_path');\n\n\tconst pool = new pg.Pool({\n\t\tconnectionString: parsed.toString(),\n\t\t...(searchPath ? { options: `-c search_path=${searchPath}` } : {}),\n\t\tfallback_application_name: applicationName(),\n\t});\n\n\tpool.on('error', (error) => {\n\t\tconst logger = currentRequestContext()?.logger;\n\t\tif (logger) logger.warn({ err: error }, 'Idle database connection lost');\n\t\telse console.warn('Idle database connection lost:', error.message);\n\t});\n\tif (options.queryTags !== false) {\n\t\tpool.on('connect', (client) => tagQueries(client));\n\t}\n\n\topen.add(pool);\n\tconst end = pool.end.bind(pool);\n\tpool.end = (async () => {\n\t\topen.delete(pool);\n\t\tawait end();\n\t}) as typeof pool.end;\n\n\treturn pool;\n}\n\n/**\n * End every pool opened, waiting for checked-out connections to come back.\n * For a server's shutdown, after it has stopped taking requests.\n */\nexport async function closeDatabasePools(): Promise<void> {\n\tawait Promise.all([...open].map((pool) => pool.end()));\n}\n\n/**\n * Who this process is, as Postgres will show it: the Lambda function, else the\n * app — `GKM_APP_NAME` where an entry sets it, `gkm dev`'s app tag otherwise.\n * Postgres keeps 63 bytes of it.\n */\nexport function applicationName(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n\treturn (\n\t\tenv.AWS_LAMBDA_FUNCTION_NAME ||\n\t\tenv.GKM_APP_NAME ||\n\t\tenv.GKM_DEV_APP?.split('#').pop() ||\n\t\tundefined\n\t);\n}\n\n/**\n * The comment that says what ran a query, or '' outside a request.\n *\n * Values keep only characters that cannot end a comment or a quoted value —\n * `*` and `'` among those dropped — so a route cannot inject SQL through it.\n */\nexport function queryTag(): string {\n\tconst request = currentRequestContext();\n\tif (!request) return '';\n\n\tconst tags = [\n\t\t['operation', request.operation],\n\t\t['request_id', request.requestId],\n\t].filter((tag): tag is [string, string] => Boolean(tag[1]));\n\tif (tags.length === 0) return '';\n\n\tconst clean = (value: string) => value.replace(/[^\\w ./:{}@-]/g, '');\n\treturn ` /*${tags.map(([key, value]) => `${key}='${clean(value)}'`).join(',')}*/`;\n}\n\n/**\n * Append {@link queryTag} to everything `client` runs: a SQL string — what\n * Kysely sends — or a query object such as a cursor's.\n */\nfunction tagQueries(client: pg.PoolClient): void {\n\tconst query = client.query.bind(client) as (...args: unknown[]) => unknown;\n\tclient.query = ((first: unknown, ...rest: unknown[]) => {\n\t\tconst tag = queryTag();\n\t\tif (tag && typeof first === 'string') return query(first + tag, ...rest);\n\t\tif (\n\t\t\ttag &&\n\t\t\tfirst &&\n\t\t\ttypeof first === 'object' &&\n\t\t\ttypeof (first as { text?: unknown }).text === 'string'\n\t\t) {\n\t\t\tconst config = first as { text: string };\n\t\t\tconfig.text += tag;\n\t\t}\n\t\treturn query(first, ...rest);\n\t}) as typeof client.query;\n}\n","/**\n * `KyselyDatabase` — a declared Postgres database, typed by its schema.\n *\n * It declares a logical database, a schema, and the roles that reach it, and\n * hands back a Kysely client built from the one URL the adapter supplied. It\n * names no cloud: `--target=aws` fills that URL from RDS, local dev from the\n * Postgres container.\n *\n * Transactions and RLS context are the execution wrapper's job — it opens the\n * transaction and puts it in `ctx.db`. This construct does not resolve\n * connections per request.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\tDEFAULT_POSTGRES_VERSION,\n\ttype Declaration,\n\ttype PostgresVersion,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport { Kysely, type KyselyConfig, PostgresDialect } from 'kysely';\nimport { Cache } from '../cache';\nimport type { Construct } from '../construct-interface';\nimport { openPool } from './pool';\n\n/** The schema a database uses when it does not say otherwise. */\nconst DEFAULT_SCHEMA = 'app';\n\n/**\n * Everything Kysely takes, plus what this construct declares.\n *\n * `dialect` is omitted because the construct owns it — it is built from the one\n * URL the adapter supplied, which is the whole point of the construct. The rest\n * of `KyselyConfig` (`plugins`, `log`) is the app's business and passes\n * straight through, so anything Kysely adds later arrives without being\n * tracked here.\n */\nexport interface KyselyDatabaseOptions extends Omit<KyselyConfig, 'dialect'> {\n\t/**\n\t * The schema, pinned on both roles' `search_path`.\n\t *\n\t * Defaults to `app` — the role the schema plays, rather than a restatement\n\t * of the database's own name, which reads correctly beside `auth` and\n\t * `pgboss` and keeps the application off `public`.\n\t */\n\tschema?: string;\n\t/**\n\t * Provision the owner/runtime role split. Defaults to on.\n\t *\n\t * Off falls back to the cluster's master credential in both URLs, giving up\n\t * the DDL/DML split and putting credentials that can drop a table into the\n\t * function's environment. A deliberate downgrade, not a default.\n\t */\n\troles?: boolean;\n\t/**\n\t * The Postgres major version to provision.\n\t *\n\t * Declared here so one statement reaches both targets. It was previously\n\t * set in two unrelated places — a container tag locally, nothing at all on\n\t * AWS (so Aurora chose) — which is how local came to run 18 while deployed\n\t * ran 17.7 with nothing recording the difference.\n\t *\n\t * Defaults to {@link DEFAULT_POSTGRES_VERSION}.\n\t */\n\tversion?: PostgresVersion;\n\t/**\n\t * End each query run inside a request with a comment naming what ran it —\n\t * `/*operation='POST /orders',request_id='…'*\\/` — so `pg_stat_activity`\n\t * and the server's logs can tie a query to its endpoint. Defaults to on.\n\t */\n\tqueryTags?: boolean;\n}\n\n/** What makes a construct derived rather than a database in its own right. */\ninterface DerivedFrom {\n\tkind: 'database-reader' | 'database-schema';\n\tof: string;\n\tschema?: string;\n}\n\n/**\n * @typeParam DB - the database's schema.\n * @typeParam TName - the canonical id. TypeScript has no partial\n * type-argument inference, so `new KyselyDatabase<OrdersDB>('orders')` leaves\n * this at its default and the service key widens to `string`. `db` still types\n * through `.database()`, which infers from the service.\n */\nexport class KyselyDatabase<DB = unknown, TName extends string = string>\n\timplements Construct<TName, Kysely<DB>>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, Kysely<DB>>;\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the key the\n\t * build publishes and the key the client reads cannot drift.\n\t */\n\tprivate readonly config: { url: string; ownerUrl: string };\n\tprivate readonly schemaName: string;\n\tprivate readonly roles: boolean;\n\tprivate readonly version: PostgresVersion;\n\tprivate readonly derivedFrom?: DerivedFrom;\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly options: KyselyDatabaseOptions = {},\n\t\tderivedFrom?: DerivedFrom,\n\t) {\n\t\t// Canonicalises, so `orders` and `Orders` are one construct rather than\n\t\t// two that collide. Throws on anything that cannot become a valid id.\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.config = {\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\townerUrl: provideKey(canonical, 'ownerUrl'),\n\t\t};\n\t\tthis.schemaName = options.schema ?? DEFAULT_SCHEMA;\n\t\tthis.roles = options.roles ?? true;\n\t\tthis.version = options.version ?? DEFAULT_POSTGRES_VERSION;\n\t\tthis.derivedFrom = derivedFrom;\n\n\t\t// A field, not a getter: service discovery caches by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\t// Forwards the whole options object — cherry-picking `envParser` strips\n\t\t\t// `context` and with it the request-scoped logger.\n\t\t\tregister: (registerOptions) => this.connect(registerOptions),\n\t\t};\n\t}\n\n\tdeclare(): Declaration[] {\n\t\tif (this.derivedFrom) {\n\t\t\tconst { kind, of, schema } = this.derivedFrom;\n\n\t\t\treturn [\n\t\t\t\tkind === 'database-schema'\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\tid: this.id,\n\t\t\t\t\t\t\tof,\n\t\t\t\t\t\t\tschema: schema ?? this.schemaName,\n\t\t\t\t\t\t\t// The runtime key alone — see the note below on the parent.\n\t\t\t\t\t\t\tprovides: [this.config.url],\n\t\t\t\t\t\t}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\tid: this.id,\n\t\t\t\t\t\t\tof,\n\t\t\t\t\t\t\t// A reader has no owner URL of its own — nothing migrates\n\t\t\t\t\t\t\t// through a read-only endpoint — so this is the one key.\n\t\t\t\t\t\t\tprovides: [this.config.url],\n\t\t\t\t\t\t},\n\t\t\t];\n\t\t}\n\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'database',\n\t\t\t\tid: this.id,\n\t\t\t\tengine: 'postgres',\n\t\t\t\tschema: this.schemaName,\n\t\t\t\tversion: this.version,\n\t\t\t\t// One key, the runtime role's. `config` also holds the owner key and\n\t\t\t\t// it is deliberately absent here: a key in `provides` is a key an\n\t\t\t\t// edge can name, and nothing should be able to depend on DDL rights.\n\t\t\t\tprovides: [this.config.url],\n\t\t\t\t...(this.roles ? {} : { roles: false }),\n\t\t\t},\n\t\t];\n\t}\n\n\t/**\n\t * A read-only endpoint on this database.\n\t *\n\t * Read-only is enforced by the reader role's grants rather than by which\n\t * endpoint it resolves to, so falling back to the writer where no replica\n\t * exists is safe rather than a silently writable connection behind a name\n\t * that says reader. There is no `writer()`: the database *is* the writer, so\n\t * a replica can never be reached by accident.\n\t *\n\t * The return type is narrower than this class on purpose — see\n\t * {@link ReadOnlyDatabase}.\n\t */\n\treader(): ReadOnlyDatabase<DB, `${TName}Reader`> {\n\t\treturn new KyselyDatabase<DB, `${TName}Reader`>(\n\t\t\t`${this.id}Reader` as ConstructName<`${TName}Reader`>,\n\t\t\tthis.options,\n\t\t\t{ kind: 'database-reader', of: this.id },\n\t\t);\n\t}\n\n\t/**\n\t * A cache that lives in this database.\n\t *\n\t * The same strengthening `schema()` is over a second database: `new\n\t * Cache('Sessions')` says the app caches and leaves where to the deployment,\n\t * while this says it caches *here* — which is a fact about the application\n\t * and so belongs in its code.\n\t *\n\t * What it buys beyond being explicit: the table's schema and the role that\n\t * reaches it come from this database rather than from a second convention,\n\t * and the backend no longer has to guess which database \"the database\" meant\n\t * when an app declares two.\n\t *\n\t * A cache is not a `KyselyDatabase`, so this returns a `Cache` — the client\n\t * is a key/value store, not a query builder, and typing it as the parent\n\t * would be a convenience that lies.\n\t */\n\tcache<TCache extends string = `${TName}Cache`>(\n\t\tid: ConstructName<TCache> = `${this.id}Cache` as ConstructName<TCache>,\n\t\toptions: { table?: string } = {},\n\t): Cache<TCache> {\n\t\treturn new Cache<TCache>(id, {\n\t\t\tof: this.id,\n\t\t\t...(options.table ? { table: options.table } : {}),\n\t\t});\n\t}\n\n\t/**\n\t * A second schema in this database, with its own role(s) and its own URL —\n\t * so this database's role holds no grant on those tables at all.\n\t *\n\t * Typed separately because a tenant has its own tables: the point is that\n\t * `auth.selectFrom('sessions')` type-checks and\n\t * `orders.selectFrom('sessions')` does not.\n\t */\n\tschema<TenantDB, TTenant extends string>(\n\t\tid: ConstructName<TTenant>,\n\t\toptions: KyselyDatabaseOptions = {},\n\t): KyselyDatabase<TenantDB, TTenant> {\n\t\treturn new KyselyDatabase<TenantDB, TTenant>(id, options, {\n\t\t\tkind: 'database-schema',\n\t\t\tof: this.id,\n\t\t\tschema: options.schema ?? (id as string).toLowerCase(),\n\t\t});\n\t}\n\n\t/**\n\t * A connection as the *owner* role — the one that may create, alter and drop.\n\t *\n\t * A `Service` rather than part of the construct, and deliberately so: a\n\t * construct is what `.dependsOn()` accepts, and nothing should be able to\n\t * depend on this. A migrator asks for it by name; a handler cannot ask at\n\t * all, which is the security property the role split exists for.\n\t *\n\t * Falls back to the runtime URL where no owner URL was published — the\n\t * `roles: false` downgrade, where there is only one credential and it is the\n\t * cluster master's.\n\t */\n\tget owner(): Service<`${Uncapitalize<TName>}Owner`, Kysely<DB>> {\n\t\treturn {\n\t\t\tserviceName:\n\t\t\t\t`${serviceKey(this.id)}Owner` as `${Uncapitalize<TName>}Owner`,\n\t\t\tregister: (options) => this.connect(options, { owner: true }),\n\t\t};\n\t}\n\n\t/**\n\t * What this database's clients are built with besides a dialect — the\n\t * plugins and logging it was declared with.\n\t *\n\t * Public so that whatever opens a connection to it outside \\`connect()\\` —\n\t * a test's transaction — builds the same client. A test that dropped the\n\t * plugins would write \\`createdAt\\` where production writes \\`created_at\\`.\n\t */\n\tget clientConfig(): Omit<KyselyConfig, 'dialect'> {\n\t\t// Split what the manifest declared from what Kysely takes, so neither\n\t\t// leaks into the other: \\`schema\\`, \\`roles\\` and \\`version\\` describe\n\t\t// infrastructure and mean nothing to a client.\n\t\tconst {\n\t\t\tschema: _schema,\n\t\t\troles: _roles,\n\t\t\tversion: _version,\n\t\t\tqueryTags: _queryTags,\n\t\t\t...kysely\n\t\t} = this.options;\n\t\treturn kysely;\n\t}\n\n\tprivate async connect(\n\t\toptions: ServiceRegisterOptions,\n\t\tas: { owner?: boolean } = {},\n\t): Promise<Kysely<DB>> {\n\t\tconst { url, ownerUrl } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\turl: get(this.config.url).string(),\n\t\t\t\townerUrl: get(this.config.ownerUrl).string().optional(),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\treturn new Kysely<DB>({\n\t\t\t...this.clientConfig,\n\t\t\tdialect: new PostgresDialect({\n\t\t\t\tpool: openPool(as.owner ? (ownerUrl ?? url) : url, {\n\t\t\t\t\tqueryTags: this.options.queryTags,\n\t\t\t\t}),\n\t\t\t}),\n\t\t});\n\t}\n}\n\n/**\n * A database you may read through, and derive nothing from.\n *\n * A reader is terminal in the graph, and this is that fact in the type system.\n * `DERIVES_FROM` gives `cache` and `database-schema` no `database-reader`\n * parent, and there is no reader of a reader — so `orders.reader().cache(…)`\n * has always been illegal, but it type-checked and failed later at\n * `assertDerivations`. That is a long way round to learn that a read-only\n * endpoint cannot own a table something writes to.\n *\n * Omitting the three deriving methods rather than making them throw keeps the\n * failure where it costs least: the editor, on the line that wrote it.\n *\n * The result is deliberately not assignable back to `KyselyDatabase` — a\n * reader is not one, and anything that genuinely needs the writer should say\n * so.\n */\nexport type ReadOnlyDatabase<DB, TName extends string> = Omit<\n\tKyselyDatabase<DB, TName>,\n\t'reader' | 'cache' | 'schema'\n>;\nexport { closeDatabasePools } from './pool';\n"],"mappings":"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"}
@@ -49,7 +49,7 @@ function openPool(url, options = {}) {
49
49
  });
50
50
  pool.on("error", (error) => {
51
51
  const logger = currentRequestContext()?.logger;
52
- if (logger) logger.warn({ error }, "Idle database connection lost");
52
+ if (logger) logger.warn({ err: error }, "Idle database connection lost");
53
53
  else console.warn("Idle database connection lost:", error.message);
54
54
  });
55
55
  if (options.queryTags !== false) pool.on("connect", (client) => tagQueries(client));
@@ -285,4 +285,4 @@ var KyselyDatabase = class KyselyDatabase {
285
285
  //#endregion
286
286
  export { closeDatabasePools as n, KyselyDatabase as t };
287
287
 
288
- //# sourceMappingURL=kysely-BYfM6Zby.mjs.map
288
+ //# sourceMappingURL=kysely-BhJ3lLdE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kysely-BhJ3lLdE.mjs","names":[],"sources":["../src/database/pool.ts","../src/database/kysely.ts"],"sourcesContent":["/**\n * The `pg` pools a database construct opens, and what every one of them gets:\n *\n * - **A name.** `application_name` is what `pg_stat_activity` shows beside a\n * connection. Without one, a database at `max_connections` cannot say who\n * holds them. It is a *fallback*, so `PGAPPNAME` or `?application_name=` in\n * the URL still win.\n * - **Tagged queries.** Inside a request, each query ends in a comment naming\n * what ran it — `/*operation='POST /orders',request_id='…'*\\/` — in the\n * sqlcommenter form. It shows in `pg_stat_activity.query`, in the server's\n * slow-query and `auto_explain` logs, and costs no round trip;\n * `pg_stat_statements` ignores comments, so its grouping is unchanged.\n * - **A listener for idle-client errors.** When the server ends an idle\n * connection (`idle_session_timeout`, a failover, an admin), the pool emits\n * `'error'`. With no listener that is an uncaught exception that kills the\n * process — for a client the pool has already discarded.\n * - **A way to close.** {@link closeDatabasePools} ends every pool opened, so a\n * server shutting down releases its connections instead of holding them\n * until the process dies.\n */\n\nimport { currentRequestContext } from '@geekmidas/services';\nimport pg from 'pg';\nimport { onShutdown } from '../shutdown';\n\nexport interface DatabasePoolOptions {\n\t/** Tag queries run inside a request with what ran them. Default true. */\n\tqueryTags?: boolean;\n}\n\n/** Every pool opened and not yet closed, so a shutdown can end them all. */\nconst open = new Set<pg.Pool>();\nonShutdown(() => closeDatabasePools());\n\n/**\n * A pool for one URL, with its schema actually on the search path.\n *\n * `?search_path=` is not a libpq parameter — a URL carrying it connects\n * happily and then resolves every unqualified name against `public`, which is\n * how a schema tenant silently writes its tables into the database it was\n * separated from. Postgres takes it as a startup option instead, so the URL\n * keeps the readable form the target derives and this turns it into the thing\n * the server understands.\n */\nexport function openPool(\n\turl: string,\n\toptions: DatabasePoolOptions = {},\n): pg.Pool {\n\tconst parsed = new URL(url);\n\tconst searchPath = parsed.searchParams.get('search_path');\n\tparsed.searchParams.delete('search_path');\n\n\tconst pool = new pg.Pool({\n\t\tconnectionString: parsed.toString(),\n\t\t...(searchPath ? { options: `-c search_path=${searchPath}` } : {}),\n\t\tfallback_application_name: applicationName(),\n\t});\n\n\tpool.on('error', (error) => {\n\t\tconst logger = currentRequestContext()?.logger;\n\t\tif (logger) logger.warn({ err: error }, 'Idle database connection lost');\n\t\telse console.warn('Idle database connection lost:', error.message);\n\t});\n\tif (options.queryTags !== false) {\n\t\tpool.on('connect', (client) => tagQueries(client));\n\t}\n\n\topen.add(pool);\n\tconst end = pool.end.bind(pool);\n\tpool.end = (async () => {\n\t\topen.delete(pool);\n\t\tawait end();\n\t}) as typeof pool.end;\n\n\treturn pool;\n}\n\n/**\n * End every pool opened, waiting for checked-out connections to come back.\n * For a server's shutdown, after it has stopped taking requests.\n */\nexport async function closeDatabasePools(): Promise<void> {\n\tawait Promise.all([...open].map((pool) => pool.end()));\n}\n\n/**\n * Who this process is, as Postgres will show it: the Lambda function, else the\n * app — `GKM_APP_NAME` where an entry sets it, `gkm dev`'s app tag otherwise.\n * Postgres keeps 63 bytes of it.\n */\nexport function applicationName(\n\tenv: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n\treturn (\n\t\tenv.AWS_LAMBDA_FUNCTION_NAME ||\n\t\tenv.GKM_APP_NAME ||\n\t\tenv.GKM_DEV_APP?.split('#').pop() ||\n\t\tundefined\n\t);\n}\n\n/**\n * The comment that says what ran a query, or '' outside a request.\n *\n * Values keep only characters that cannot end a comment or a quoted value —\n * `*` and `'` among those dropped — so a route cannot inject SQL through it.\n */\nexport function queryTag(): string {\n\tconst request = currentRequestContext();\n\tif (!request) return '';\n\n\tconst tags = [\n\t\t['operation', request.operation],\n\t\t['request_id', request.requestId],\n\t].filter((tag): tag is [string, string] => Boolean(tag[1]));\n\tif (tags.length === 0) return '';\n\n\tconst clean = (value: string) => value.replace(/[^\\w ./:{}@-]/g, '');\n\treturn ` /*${tags.map(([key, value]) => `${key}='${clean(value)}'`).join(',')}*/`;\n}\n\n/**\n * Append {@link queryTag} to everything `client` runs: a SQL string — what\n * Kysely sends — or a query object such as a cursor's.\n */\nfunction tagQueries(client: pg.PoolClient): void {\n\tconst query = client.query.bind(client) as (...args: unknown[]) => unknown;\n\tclient.query = ((first: unknown, ...rest: unknown[]) => {\n\t\tconst tag = queryTag();\n\t\tif (tag && typeof first === 'string') return query(first + tag, ...rest);\n\t\tif (\n\t\t\ttag &&\n\t\t\tfirst &&\n\t\t\ttypeof first === 'object' &&\n\t\t\ttypeof (first as { text?: unknown }).text === 'string'\n\t\t) {\n\t\t\tconst config = first as { text: string };\n\t\t\tconfig.text += tag;\n\t\t}\n\t\treturn query(first, ...rest);\n\t}) as typeof client.query;\n}\n","/**\n * `KyselyDatabase` — a declared Postgres database, typed by its schema.\n *\n * It declares a logical database, a schema, and the roles that reach it, and\n * hands back a Kysely client built from the one URL the adapter supplied. It\n * names no cloud: `--target=aws` fills that URL from RDS, local dev from the\n * Postgres container.\n *\n * Transactions and RLS context are the execution wrapper's job — it opens the\n * transaction and puts it in `ctx.db`. This construct does not resolve\n * connections per request.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\tDEFAULT_POSTGRES_VERSION,\n\ttype Declaration,\n\ttype PostgresVersion,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport { Kysely, type KyselyConfig, PostgresDialect } from 'kysely';\nimport { Cache } from '../cache';\nimport type { Construct } from '../construct-interface';\nimport { openPool } from './pool';\n\n/** The schema a database uses when it does not say otherwise. */\nconst DEFAULT_SCHEMA = 'app';\n\n/**\n * Everything Kysely takes, plus what this construct declares.\n *\n * `dialect` is omitted because the construct owns it — it is built from the one\n * URL the adapter supplied, which is the whole point of the construct. The rest\n * of `KyselyConfig` (`plugins`, `log`) is the app's business and passes\n * straight through, so anything Kysely adds later arrives without being\n * tracked here.\n */\nexport interface KyselyDatabaseOptions extends Omit<KyselyConfig, 'dialect'> {\n\t/**\n\t * The schema, pinned on both roles' `search_path`.\n\t *\n\t * Defaults to `app` — the role the schema plays, rather than a restatement\n\t * of the database's own name, which reads correctly beside `auth` and\n\t * `pgboss` and keeps the application off `public`.\n\t */\n\tschema?: string;\n\t/**\n\t * Provision the owner/runtime role split. Defaults to on.\n\t *\n\t * Off falls back to the cluster's master credential in both URLs, giving up\n\t * the DDL/DML split and putting credentials that can drop a table into the\n\t * function's environment. A deliberate downgrade, not a default.\n\t */\n\troles?: boolean;\n\t/**\n\t * The Postgres major version to provision.\n\t *\n\t * Declared here so one statement reaches both targets. It was previously\n\t * set in two unrelated places — a container tag locally, nothing at all on\n\t * AWS (so Aurora chose) — which is how local came to run 18 while deployed\n\t * ran 17.7 with nothing recording the difference.\n\t *\n\t * Defaults to {@link DEFAULT_POSTGRES_VERSION}.\n\t */\n\tversion?: PostgresVersion;\n\t/**\n\t * End each query run inside a request with a comment naming what ran it —\n\t * `/*operation='POST /orders',request_id='…'*\\/` — so `pg_stat_activity`\n\t * and the server's logs can tie a query to its endpoint. Defaults to on.\n\t */\n\tqueryTags?: boolean;\n}\n\n/** What makes a construct derived rather than a database in its own right. */\ninterface DerivedFrom {\n\tkind: 'database-reader' | 'database-schema';\n\tof: string;\n\tschema?: string;\n}\n\n/**\n * @typeParam DB - the database's schema.\n * @typeParam TName - the canonical id. TypeScript has no partial\n * type-argument inference, so `new KyselyDatabase<OrdersDB>('orders')` leaves\n * this at its default and the service key widens to `string`. `db` still types\n * through `.database()`, which infers from the service.\n */\nexport class KyselyDatabase<DB = unknown, TName extends string = string>\n\timplements Construct<TName, Kysely<DB>>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, Kysely<DB>>;\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the key the\n\t * build publishes and the key the client reads cannot drift.\n\t */\n\tprivate readonly config: { url: string; ownerUrl: string };\n\tprivate readonly schemaName: string;\n\tprivate readonly roles: boolean;\n\tprivate readonly version: PostgresVersion;\n\tprivate readonly derivedFrom?: DerivedFrom;\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly options: KyselyDatabaseOptions = {},\n\t\tderivedFrom?: DerivedFrom,\n\t) {\n\t\t// Canonicalises, so `orders` and `Orders` are one construct rather than\n\t\t// two that collide. Throws on anything that cannot become a valid id.\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.config = {\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\townerUrl: provideKey(canonical, 'ownerUrl'),\n\t\t};\n\t\tthis.schemaName = options.schema ?? DEFAULT_SCHEMA;\n\t\tthis.roles = options.roles ?? true;\n\t\tthis.version = options.version ?? DEFAULT_POSTGRES_VERSION;\n\t\tthis.derivedFrom = derivedFrom;\n\n\t\t// A field, not a getter: service discovery caches by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\t// Forwards the whole options object — cherry-picking `envParser` strips\n\t\t\t// `context` and with it the request-scoped logger.\n\t\t\tregister: (registerOptions) => this.connect(registerOptions),\n\t\t};\n\t}\n\n\tdeclare(): Declaration[] {\n\t\tif (this.derivedFrom) {\n\t\t\tconst { kind, of, schema } = this.derivedFrom;\n\n\t\t\treturn [\n\t\t\t\tkind === 'database-schema'\n\t\t\t\t\t? {\n\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\tid: this.id,\n\t\t\t\t\t\t\tof,\n\t\t\t\t\t\t\tschema: schema ?? this.schemaName,\n\t\t\t\t\t\t\t// The runtime key alone — see the note below on the parent.\n\t\t\t\t\t\t\tprovides: [this.config.url],\n\t\t\t\t\t\t}\n\t\t\t\t\t: {\n\t\t\t\t\t\t\tkind,\n\t\t\t\t\t\t\tid: this.id,\n\t\t\t\t\t\t\tof,\n\t\t\t\t\t\t\t// A reader has no owner URL of its own — nothing migrates\n\t\t\t\t\t\t\t// through a read-only endpoint — so this is the one key.\n\t\t\t\t\t\t\tprovides: [this.config.url],\n\t\t\t\t\t\t},\n\t\t\t];\n\t\t}\n\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'database',\n\t\t\t\tid: this.id,\n\t\t\t\tengine: 'postgres',\n\t\t\t\tschema: this.schemaName,\n\t\t\t\tversion: this.version,\n\t\t\t\t// One key, the runtime role's. `config` also holds the owner key and\n\t\t\t\t// it is deliberately absent here: a key in `provides` is a key an\n\t\t\t\t// edge can name, and nothing should be able to depend on DDL rights.\n\t\t\t\tprovides: [this.config.url],\n\t\t\t\t...(this.roles ? {} : { roles: false }),\n\t\t\t},\n\t\t];\n\t}\n\n\t/**\n\t * A read-only endpoint on this database.\n\t *\n\t * Read-only is enforced by the reader role's grants rather than by which\n\t * endpoint it resolves to, so falling back to the writer where no replica\n\t * exists is safe rather than a silently writable connection behind a name\n\t * that says reader. There is no `writer()`: the database *is* the writer, so\n\t * a replica can never be reached by accident.\n\t *\n\t * The return type is narrower than this class on purpose — see\n\t * {@link ReadOnlyDatabase}.\n\t */\n\treader(): ReadOnlyDatabase<DB, `${TName}Reader`> {\n\t\treturn new KyselyDatabase<DB, `${TName}Reader`>(\n\t\t\t`${this.id}Reader` as ConstructName<`${TName}Reader`>,\n\t\t\tthis.options,\n\t\t\t{ kind: 'database-reader', of: this.id },\n\t\t);\n\t}\n\n\t/**\n\t * A cache that lives in this database.\n\t *\n\t * The same strengthening `schema()` is over a second database: `new\n\t * Cache('Sessions')` says the app caches and leaves where to the deployment,\n\t * while this says it caches *here* — which is a fact about the application\n\t * and so belongs in its code.\n\t *\n\t * What it buys beyond being explicit: the table's schema and the role that\n\t * reaches it come from this database rather than from a second convention,\n\t * and the backend no longer has to guess which database \"the database\" meant\n\t * when an app declares two.\n\t *\n\t * A cache is not a `KyselyDatabase`, so this returns a `Cache` — the client\n\t * is a key/value store, not a query builder, and typing it as the parent\n\t * would be a convenience that lies.\n\t */\n\tcache<TCache extends string = `${TName}Cache`>(\n\t\tid: ConstructName<TCache> = `${this.id}Cache` as ConstructName<TCache>,\n\t\toptions: { table?: string } = {},\n\t): Cache<TCache> {\n\t\treturn new Cache<TCache>(id, {\n\t\t\tof: this.id,\n\t\t\t...(options.table ? { table: options.table } : {}),\n\t\t});\n\t}\n\n\t/**\n\t * A second schema in this database, with its own role(s) and its own URL —\n\t * so this database's role holds no grant on those tables at all.\n\t *\n\t * Typed separately because a tenant has its own tables: the point is that\n\t * `auth.selectFrom('sessions')` type-checks and\n\t * `orders.selectFrom('sessions')` does not.\n\t */\n\tschema<TenantDB, TTenant extends string>(\n\t\tid: ConstructName<TTenant>,\n\t\toptions: KyselyDatabaseOptions = {},\n\t): KyselyDatabase<TenantDB, TTenant> {\n\t\treturn new KyselyDatabase<TenantDB, TTenant>(id, options, {\n\t\t\tkind: 'database-schema',\n\t\t\tof: this.id,\n\t\t\tschema: options.schema ?? (id as string).toLowerCase(),\n\t\t});\n\t}\n\n\t/**\n\t * A connection as the *owner* role — the one that may create, alter and drop.\n\t *\n\t * A `Service` rather than part of the construct, and deliberately so: a\n\t * construct is what `.dependsOn()` accepts, and nothing should be able to\n\t * depend on this. A migrator asks for it by name; a handler cannot ask at\n\t * all, which is the security property the role split exists for.\n\t *\n\t * Falls back to the runtime URL where no owner URL was published — the\n\t * `roles: false` downgrade, where there is only one credential and it is the\n\t * cluster master's.\n\t */\n\tget owner(): Service<`${Uncapitalize<TName>}Owner`, Kysely<DB>> {\n\t\treturn {\n\t\t\tserviceName:\n\t\t\t\t`${serviceKey(this.id)}Owner` as `${Uncapitalize<TName>}Owner`,\n\t\t\tregister: (options) => this.connect(options, { owner: true }),\n\t\t};\n\t}\n\n\t/**\n\t * What this database's clients are built with besides a dialect — the\n\t * plugins and logging it was declared with.\n\t *\n\t * Public so that whatever opens a connection to it outside \\`connect()\\` —\n\t * a test's transaction — builds the same client. A test that dropped the\n\t * plugins would write \\`createdAt\\` where production writes \\`created_at\\`.\n\t */\n\tget clientConfig(): Omit<KyselyConfig, 'dialect'> {\n\t\t// Split what the manifest declared from what Kysely takes, so neither\n\t\t// leaks into the other: \\`schema\\`, \\`roles\\` and \\`version\\` describe\n\t\t// infrastructure and mean nothing to a client.\n\t\tconst {\n\t\t\tschema: _schema,\n\t\t\troles: _roles,\n\t\t\tversion: _version,\n\t\t\tqueryTags: _queryTags,\n\t\t\t...kysely\n\t\t} = this.options;\n\t\treturn kysely;\n\t}\n\n\tprivate async connect(\n\t\toptions: ServiceRegisterOptions,\n\t\tas: { owner?: boolean } = {},\n\t): Promise<Kysely<DB>> {\n\t\tconst { url, ownerUrl } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\turl: get(this.config.url).string(),\n\t\t\t\townerUrl: get(this.config.ownerUrl).string().optional(),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\treturn new Kysely<DB>({\n\t\t\t...this.clientConfig,\n\t\t\tdialect: new PostgresDialect({\n\t\t\t\tpool: openPool(as.owner ? (ownerUrl ?? url) : url, {\n\t\t\t\t\tqueryTags: this.options.queryTags,\n\t\t\t\t}),\n\t\t\t}),\n\t\t});\n\t}\n}\n\n/**\n * A database you may read through, and derive nothing from.\n *\n * A reader is terminal in the graph, and this is that fact in the type system.\n * `DERIVES_FROM` gives `cache` and `database-schema` no `database-reader`\n * parent, and there is no reader of a reader — so `orders.reader().cache(…)`\n * has always been illegal, but it type-checked and failed later at\n * `assertDerivations`. That is a long way round to learn that a read-only\n * endpoint cannot own a table something writes to.\n *\n * Omitting the three deriving methods rather than making them throw keeps the\n * failure where it costs least: the editor, on the line that wrote it.\n *\n * The result is deliberately not assignable back to `KyselyDatabase` — a\n * reader is not one, and anything that genuinely needs the writer should say\n * so.\n */\nexport type ReadOnlyDatabase<DB, TName extends string> = Omit<\n\tKyselyDatabase<DB, TName>,\n\t'reader' | 'cache' | 'schema'\n>;\nexport { closeDatabasePools } from './pool';\n"],"mappings":"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"}
@@ -1,4 +1,57 @@
1
- import { DefaultAuditor } from "@geekmidas/audit";
1
+ //#region src/endpoints/optionalPeers.ts
2
+ /**
3
+ * The optional peers an endpoint needs only for a feature it opted into.
4
+ *
5
+ * `@geekmidas/audit`, `@geekmidas/rate-limit` and `@geekmidas/db` are optional
6
+ * peers of this package: an app that audits nothing, limits nothing and sets
7
+ * no row-level security need not install them. The adaptor every endpoint is
8
+ * served by used to import them at the top, so serving any endpoint needed
9
+ * them all — and a production bundle of an app without them failed to build.
10
+ * They are loaded here, when an endpoint that uses one is served, which is
11
+ * also when the app declaring that endpoint has installed it: an auditor's
12
+ * storage, a rate limit's store and an RLS policy's database come from these
13
+ * packages.
14
+ *
15
+ * The `try` is load-bearing beyond the error it names: a bundler leaves an
16
+ * import it cannot resolve to run time when the import is guarded, and fails
17
+ * the build when it is not.
18
+ */
19
+ /** An endpoint uses a feature whose optional peer is not installed. */
20
+ var OptionalPeerMissing = class extends Error {
21
+ packageName;
22
+ feature;
23
+ constructor(packageName, feature, cause) {
24
+ super(`${feature} needs '${packageName}', which could not be loaded. Add it to the dependencies of the app that serves this endpoint.`, { cause });
25
+ this.packageName = packageName;
26
+ this.feature = feature;
27
+ this.name = "OptionalPeerMissing";
28
+ }
29
+ };
30
+ /** `@geekmidas/audit`, for an endpoint with an auditor. */
31
+ async function loadAudit() {
32
+ try {
33
+ return await import("@geekmidas/audit");
34
+ } catch (cause) {
35
+ throw new OptionalPeerMissing("@geekmidas/audit", "An endpoint with an auditor", cause);
36
+ }
37
+ }
38
+ /** `@geekmidas/rate-limit`, for an endpoint with a rate limit. */
39
+ async function loadRateLimit() {
40
+ try {
41
+ return await import("@geekmidas/rate-limit");
42
+ } catch (cause) {
43
+ throw new OptionalPeerMissing("@geekmidas/rate-limit", "An endpoint with a rate limit", cause);
44
+ }
45
+ }
46
+ /** `@geekmidas/db/rls`, for an endpoint with row-level security. */
47
+ async function loadRls() {
48
+ try {
49
+ return await import("@geekmidas/db/rls");
50
+ } catch (cause) {
51
+ throw new OptionalPeerMissing("@geekmidas/db", "An endpoint with row-level security", cause);
52
+ }
53
+ }
54
+ //#endregion
2
55
  //#region src/endpoints/processAudits.ts
3
56
  /**
4
57
  * Create audit context for handler execution.
@@ -28,6 +81,7 @@ async function createAuditContext(endpoint, serviceDiscovery, logger, ctx) {
28
81
  } catch (error) {
29
82
  logger.error(error, "Failed to extract actor for audits");
30
83
  }
84
+ const { DefaultAuditor } = await loadAudit();
31
85
  return {
32
86
  auditor: new DefaultAuditor({
33
87
  actor,
@@ -72,6 +126,6 @@ async function executeWithAuditTransaction(auditContext, handler, onComplete, op
72
126
  return response;
73
127
  }
74
128
  //#endregion
75
- export { executeWithAuditTransaction as n, createAuditContext as t };
129
+ export { loadRls as i, executeWithAuditTransaction as n, loadRateLimit as r, createAuditContext as t };
76
130
 
77
- //# sourceMappingURL=processAudits-BpRlPQMW.mjs.map
131
+ //# sourceMappingURL=processAudits-BRRVVHUB.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processAudits-BRRVVHUB.mjs","names":[],"sources":["../src/endpoints/optionalPeers.ts","../src/endpoints/processAudits.ts"],"sourcesContent":["/**\n * The optional peers an endpoint needs only for a feature it opted into.\n *\n * `@geekmidas/audit`, `@geekmidas/rate-limit` and `@geekmidas/db` are optional\n * peers of this package: an app that audits nothing, limits nothing and sets\n * no row-level security need not install them. The adaptor every endpoint is\n * served by used to import them at the top, so serving any endpoint needed\n * them all — and a production bundle of an app without them failed to build.\n * They are loaded here, when an endpoint that uses one is served, which is\n * also when the app declaring that endpoint has installed it: an auditor's\n * storage, a rate limit's store and an RLS policy's database come from these\n * packages.\n *\n * The `try` is load-bearing beyond the error it names: a bundler leaves an\n * import it cannot resolve to run time when the import is guarded, and fails\n * the build when it is not.\n */\n\n/** An endpoint uses a feature whose optional peer is not installed. */\nexport class OptionalPeerMissing extends Error {\n\tconstructor(\n\t\treadonly packageName: string,\n\t\treadonly feature: string,\n\t\tcause: unknown,\n\t) {\n\t\tsuper(\n\t\t\t`${feature} needs '${packageName}', which could not be loaded. ` +\n\t\t\t\t`Add it to the dependencies of the app that serves this endpoint.`,\n\t\t\t{ cause },\n\t\t);\n\t\tthis.name = 'OptionalPeerMissing';\n\t}\n}\n\n/** `@geekmidas/audit`, for an endpoint with an auditor. */\nexport async function loadAudit(): Promise<typeof import('@geekmidas/audit')> {\n\ttry {\n\t\treturn await import('@geekmidas/audit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/audit',\n\t\t\t'An endpoint with an auditor',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/rate-limit`, for an endpoint with a rate limit. */\nexport async function loadRateLimit(): Promise<\n\ttypeof import('@geekmidas/rate-limit')\n> {\n\ttry {\n\t\treturn await import('@geekmidas/rate-limit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/rate-limit',\n\t\t\t'An endpoint with a rate limit',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/db/rls`, for an endpoint with row-level security. */\nexport async function loadRls(): Promise<typeof import('@geekmidas/db/rls')> {\n\ttry {\n\t\treturn await import('@geekmidas/db/rls');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/db',\n\t\t\t'An endpoint with row-level security',\n\t\t\tcause,\n\t\t);\n\t}\n}\n","import type {\n\tAuditActor,\n\tAuditableAction,\n\tAuditor,\n\tAuditStorage,\n} from '@geekmidas/audit';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { ActorExtractor, MappedAudit } from './audit';\nimport type { CookieFn, Endpoint, HeaderFn } from './Endpoint';\nimport { loadAudit } from './optionalPeers';\n\n/**\n * Process declarative audit definitions after successful endpoint execution.\n * Similar to publishConstructEvents for events.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param response - The handler response to generate audit payloads from\n * @param serviceDiscovery - Service discovery for registering audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context (session, headers, cookies, services)\n * @param existingAuditor - Optional existing auditor instance (e.g., from handler context).\n * If provided, uses this auditor (with its stored transaction).\n * If not provided, creates a new auditor.\n */\nexport async function processEndpointAudits<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tOutSchema,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction\n\t>,\n\tresponse: InferStandardSchema<OutSchema>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n\texistingAuditor?: Auditor<TAuditAction>,\n): Promise<void> {\n\ttry {\n\t\tconst audits = endpoint.audits as MappedAudit<TAuditAction, OutSchema>[];\n\n\t\t// If we have an existing auditor (from handler context), we need to flush\n\t\t// any manual audits it collected, even if there are no declarative audits\n\t\tconst hasExistingRecords =\n\t\t\texistingAuditor && existingAuditor.getRecords().length > 0;\n\n\t\t// Skip if no declarative audits and no existing records to flush\n\t\tif (!audits?.length && !hasExistingRecords) {\n\t\t\tlogger.debug('No audits to process');\n\t\t\treturn;\n\t\t}\n\n\t\t// If no auditor storage service and we have things to process, warn\n\t\tif (!endpoint.auditorStorageService) {\n\t\t\tif (hasExistingRecords || audits?.length) {\n\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Get or create auditor\n\t\tlet auditor: Auditor<TAuditAction>;\n\n\t\tif (existingAuditor) {\n\t\t\t// Use existing auditor (preserves stored transaction and manual audits)\n\t\t\tauditor = existingAuditor;\n\t\t\tlogger.debug('Using existing auditor from handler context');\n\t\t} else {\n\t\t\t// Create new auditor (backward compatibility)\n\t\t\tconst services = await serviceDiscovery.register([\n\t\t\t\tendpoint.auditorStorageService,\n\t\t\t]);\n\t\t\tconst storage = services[\n\t\t\t\tendpoint.auditorStorageService.serviceName\n\t\t\t] as AuditStorage;\n\n\t\t\t// Extract actor if configured\n\t\t\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\t\t\tif (endpoint.actorExtractor) {\n\t\t\t\ttry {\n\t\t\t\t\tactor = await (\n\t\t\t\t\t\tendpoint.actorExtractor as ActorExtractor<\n\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\tTSession,\n\t\t\t\t\t\t\tTLogger\n\t\t\t\t\t\t>\n\t\t\t\t\t)({\n\t\t\t\t\t\tservices: ctx.services as any,\n\t\t\t\t\t\tsession: ctx.session,\n\t\t\t\t\t\theader: ctx.header,\n\t\t\t\t\t\tcookie: ctx.cookie,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t\t\t\t// Continue with system actor\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { DefaultAuditor } = await loadAudit();\n\t\t\tauditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\tactor,\n\t\t\t\tstorage,\n\t\t\t\tmetadata: {\n\t\t\t\t\tendpoint: endpoint.route,\n\t\t\t\t\tmethod: endpoint.method,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Process each declarative audit\n\t\tif (audits?.length) {\n\t\t\tfor (const audit of audits) {\n\t\t\t\tlogger.debug({ audit: audit.type }, 'Processing declarative audit');\n\n\t\t\t\t// Check when condition\n\t\t\t\tif (audit.when && !audit.when(response as any)) {\n\t\t\t\t\tlogger.debug(\n\t\t\t\t\t\t{ audit: audit.type },\n\t\t\t\t\t\t'Audit skipped due to when condition',\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Extract payload\n\t\t\t\tconst payload = audit.payload(response as any);\n\n\t\t\t\t// Extract entityId if configured\n\t\t\t\tconst entityId = audit.entityId?.(response as any);\n\n\t\t\t\t// Record the audit\n\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\ttable: audit.table,\n\t\t\t\t\tentityId,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Flush audits to storage\n\t\t// Note: If existingAuditor has a stored transaction (via setTransaction),\n\t\t// flush() will use it automatically\n\t\tconst recordCount = auditor.getRecords().length;\n\t\tif (recordCount > 0) {\n\t\t\t// Check if auditor has a stored transaction (for logging purposes)\n\t\t\tconst trx =\n\t\t\t\t'getTransaction' in auditor\n\t\t\t\t\t? (auditor as { getTransaction(): unknown }).getTransaction()\n\t\t\t\t\t: undefined;\n\t\t\tlogger.debug(\n\t\t\t\t{ auditCount: recordCount, hasTransaction: !!trx },\n\t\t\t\t'Flushing audits',\n\t\t\t);\n\t\t\tawait auditor.flush();\n\t\t}\n\t} catch (error) {\n\t\tlogger.error(error as Error, 'Failed to process audits');\n\t\t// Don't rethrow - audit failures shouldn't fail the request\n\t}\n}\n\n/**\n * Context for audit-aware handler execution.\n */\nexport interface AuditExecutionContext<\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> {\n\t/** The auditor instance for recording audits */\n\tauditor: Auditor<TAuditAction>;\n\t/** The audit storage instance */\n\tstorage: AuditStorage;\n}\n\n/**\n * Create audit context for handler execution.\n * Returns the auditor and storage for use in the handler.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param serviceDiscovery - Service discovery for getting audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context for actor extraction\n * @returns Audit context with auditor and storage, or undefined if not configured\n */\nexport async function createAuditContext<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction,\n\t\tTDatabase,\n\t\tTDatabaseServiceName\n\t>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n): Promise<AuditExecutionContext<TAuditAction> | undefined> {\n\tif (!endpoint.auditorStorageService) {\n\t\treturn undefined;\n\t}\n\n\tconst services = await serviceDiscovery.register([\n\t\tendpoint.auditorStorageService,\n\t]);\n\tconst storage = services[\n\t\tendpoint.auditorStorageService.serviceName\n\t] as AuditStorage;\n\n\t// Extract actor if configured\n\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\tif (endpoint.actorExtractor) {\n\t\ttry {\n\t\t\tactor = await (\n\t\t\t\tendpoint.actorExtractor as ActorExtractor<TServices, TSession, TLogger>\n\t\t\t)({\n\t\t\t\tservices: ctx.services as any,\n\t\t\t\tsession: ctx.session,\n\t\t\t\theader: ctx.header,\n\t\t\t\tcookie: ctx.cookie,\n\t\t\t\tlogger,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t}\n\t}\n\n\tconst { DefaultAuditor } = await loadAudit();\n\tconst auditor = new DefaultAuditor<TAuditAction>({\n\t\tactor,\n\t\tstorage,\n\t\tmetadata: {\n\t\t\tendpoint: endpoint.route,\n\t\t\tmethod: endpoint.method,\n\t\t},\n\t});\n\n\treturn { auditor, storage };\n}\n\n/**\n * Options for executeWithAuditTransaction.\n */\nexport interface ExecuteWithAuditTransactionOptions {\n\t/**\n\t * Database connection to use for the transaction.\n\t * If this is already a transaction, it will be reused instead of creating a nested one.\n\t * If not provided, the storage's internal database is used.\n\t */\n\tdb?: unknown;\n}\n\n/**\n * Execute a handler with automatic audit transaction support.\n * If the audit storage provides a withTransaction method, wraps execution\n * in a transaction so audits are atomic with handler's database operations.\n *\n * This is database-agnostic - each storage implementation provides its own\n * transaction handling based on the underlying database (Kysely, Drizzle, etc.).\n *\n * If the db parameter is provided and is already a transaction, the storage\n * will reuse it instead of creating a nested transaction (similar to\n * packages/db/src/kysely.ts#withTransaction).\n *\n * @param auditContext - The audit context from createAuditContext\n * @param handler - The handler function to execute (receives auditor)\n * @param onComplete - Called after handler with response, to process declarative audits\n * @param options - Optional configuration including database connection\n * @returns The handler result\n */\nexport async function executeWithAuditTransaction<\n\tT,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tauditContext: AuditExecutionContext<TAuditAction> | undefined,\n\thandler: (auditor?: Auditor<TAuditAction>) => Promise<T>,\n\tonComplete?: (response: T, auditor: Auditor<TAuditAction>) => Promise<void>,\n\toptions?: ExecuteWithAuditTransactionOptions,\n): Promise<T> {\n\t// No audit context - just run handler\n\tif (!auditContext) {\n\t\treturn handler(undefined);\n\t}\n\n\tconst { auditor, storage } = auditContext;\n\n\t// Check if storage provides a transaction wrapper\n\tif (storage.withTransaction) {\n\t\t// Wrap in transaction - audits are atomic with handler operations\n\t\t// The storage's withTransaction handles setTransaction and flush\n\t\t// Pass db so existing transactions are reused\n\t\treturn storage.withTransaction(\n\t\t\tauditor,\n\t\t\tasync () => {\n\t\t\t\tconst response = await handler(auditor);\n\n\t\t\t\t// Process declarative audits within the transaction\n\t\t\t\tif (onComplete) {\n\t\t\t\t\tawait onComplete(response, auditor);\n\t\t\t\t}\n\n\t\t\t\treturn response;\n\t\t\t},\n\t\t\toptions?.db,\n\t\t);\n\t}\n\n\t// No transaction support - run handler and flush audits after\n\tconst response = await handler(auditor);\n\n\tif (onComplete) {\n\t\tawait onComplete(response, auditor);\n\t}\n\n\t// Flush audits (no transaction)\n\tawait auditor.flush();\n\n\treturn response;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,IAAa,sBAAb,cAAyC,MAAM;CAEpC;CACA;CAFV,YACC,aACA,SACA,OACC;EACD,MACC,GAAG,QAAQ,UAAU,YAAY,iGAEjC,EAAE,MAAM,CACT;EARS,KAAA,cAAA;EACA,KAAA,UAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,eAAsB,YAAwD;CAC7E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,oBACA,+BACA,KACD;CACD;AACD;;AAGA,eAAsB,gBAEpB;CACD,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,yBACA,iCACA,KACD;CACD;AACD;;AAGA,eAAsB,UAAuD;CAC5E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,iBACA,uCACA,KACD;CACD;AACD;;;;;;;;;;;;;ACwIA,eAAsB,mBAarB,UAcA,kBACA,QACA,KAM2D;CAC3D,IAAI,CAAC,SAAS,uBACb;CAMD,MAAM,WAAU,MAHO,iBAAiB,SAAS,CAChD,SAAS,qBACV,CAAC,EAAA,CAEA,SAAS,sBAAsB;CAIhC,IAAI,QAAoB;EAAE,IAAI;EAAU,MAAM;CAAS;CACvD,IAAI,SAAS,gBACZ,IAAI;EACH,QAAQ,MACP,SAAS,eACR;GACD,UAAU,IAAI;GACd,SAAS,IAAI;GACb,QAAQ,IAAI;GACZ,QAAQ,IAAI;GACZ;EACD,CAAC;CACF,SAAS,OAAO;EACf,OAAO,MAAM,OAAgB,oCAAoC;CAClE;CAGD,MAAM,EAAE,mBAAmB,MAAM,UAAU;CAU3C,OAAO;EAAE,SAAA,IATW,eAA6B;GAChD;GACA;GACA,UAAU;IACT,UAAU,SAAS;IACnB,QAAQ,SAAS;GAClB;EACD,CAEe;EAAG;CAAQ;AAC3B;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,4BAOrB,cACA,SACA,YACA,SACa;CAEb,IAAI,CAAC,cACJ,OAAO,QAAQ,KAAA,CAAS;CAGzB,MAAM,EAAE,SAAS,YAAY;CAG7B,IAAI,QAAQ,iBAIX,OAAO,QAAQ,gBACd,SACA,YAAY;EACX,MAAM,WAAW,MAAM,QAAQ,OAAO;EAGtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;EAGnC,OAAO;CACR,GACA,SAAS,EACV;CAID,MAAM,WAAW,MAAM,QAAQ,OAAO;CAEtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;CAInC,MAAM,QAAQ,MAAM;CAEpB,OAAO;AACR"}
@@ -1,4 +1,57 @@
1
- let _geekmidas_audit = require("@geekmidas/audit");
1
+ //#region src/endpoints/optionalPeers.ts
2
+ /**
3
+ * The optional peers an endpoint needs only for a feature it opted into.
4
+ *
5
+ * `@geekmidas/audit`, `@geekmidas/rate-limit` and `@geekmidas/db` are optional
6
+ * peers of this package: an app that audits nothing, limits nothing and sets
7
+ * no row-level security need not install them. The adaptor every endpoint is
8
+ * served by used to import them at the top, so serving any endpoint needed
9
+ * them all — and a production bundle of an app without them failed to build.
10
+ * They are loaded here, when an endpoint that uses one is served, which is
11
+ * also when the app declaring that endpoint has installed it: an auditor's
12
+ * storage, a rate limit's store and an RLS policy's database come from these
13
+ * packages.
14
+ *
15
+ * The `try` is load-bearing beyond the error it names: a bundler leaves an
16
+ * import it cannot resolve to run time when the import is guarded, and fails
17
+ * the build when it is not.
18
+ */
19
+ /** An endpoint uses a feature whose optional peer is not installed. */
20
+ var OptionalPeerMissing = class extends Error {
21
+ packageName;
22
+ feature;
23
+ constructor(packageName, feature, cause) {
24
+ super(`${feature} needs '${packageName}', which could not be loaded. Add it to the dependencies of the app that serves this endpoint.`, { cause });
25
+ this.packageName = packageName;
26
+ this.feature = feature;
27
+ this.name = "OptionalPeerMissing";
28
+ }
29
+ };
30
+ /** `@geekmidas/audit`, for an endpoint with an auditor. */
31
+ async function loadAudit() {
32
+ try {
33
+ return await import("@geekmidas/audit");
34
+ } catch (cause) {
35
+ throw new OptionalPeerMissing("@geekmidas/audit", "An endpoint with an auditor", cause);
36
+ }
37
+ }
38
+ /** `@geekmidas/rate-limit`, for an endpoint with a rate limit. */
39
+ async function loadRateLimit() {
40
+ try {
41
+ return await import("@geekmidas/rate-limit");
42
+ } catch (cause) {
43
+ throw new OptionalPeerMissing("@geekmidas/rate-limit", "An endpoint with a rate limit", cause);
44
+ }
45
+ }
46
+ /** `@geekmidas/db/rls`, for an endpoint with row-level security. */
47
+ async function loadRls() {
48
+ try {
49
+ return await import("@geekmidas/db/rls");
50
+ } catch (cause) {
51
+ throw new OptionalPeerMissing("@geekmidas/db", "An endpoint with row-level security", cause);
52
+ }
53
+ }
54
+ //#endregion
2
55
  //#region src/endpoints/processAudits.ts
3
56
  /**
4
57
  * Create audit context for handler execution.
@@ -28,8 +81,9 @@ async function createAuditContext(endpoint, serviceDiscovery, logger, ctx) {
28
81
  } catch (error) {
29
82
  logger.error(error, "Failed to extract actor for audits");
30
83
  }
84
+ const { DefaultAuditor } = await loadAudit();
31
85
  return {
32
- auditor: new _geekmidas_audit.DefaultAuditor({
86
+ auditor: new DefaultAuditor({
33
87
  actor,
34
88
  storage,
35
89
  metadata: {
@@ -84,5 +138,17 @@ Object.defineProperty(exports, "executeWithAuditTransaction", {
84
138
  return executeWithAuditTransaction;
85
139
  }
86
140
  });
141
+ Object.defineProperty(exports, "loadRateLimit", {
142
+ enumerable: true,
143
+ get: function() {
144
+ return loadRateLimit;
145
+ }
146
+ });
147
+ Object.defineProperty(exports, "loadRls", {
148
+ enumerable: true,
149
+ get: function() {
150
+ return loadRls;
151
+ }
152
+ });
87
153
 
88
- //# sourceMappingURL=processAudits-D_2W5x8k.cjs.map
154
+ //# sourceMappingURL=processAudits-DasOZjPl.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processAudits-DasOZjPl.cjs","names":[],"sources":["../src/endpoints/optionalPeers.ts","../src/endpoints/processAudits.ts"],"sourcesContent":["/**\n * The optional peers an endpoint needs only for a feature it opted into.\n *\n * `@geekmidas/audit`, `@geekmidas/rate-limit` and `@geekmidas/db` are optional\n * peers of this package: an app that audits nothing, limits nothing and sets\n * no row-level security need not install them. The adaptor every endpoint is\n * served by used to import them at the top, so serving any endpoint needed\n * them all — and a production bundle of an app without them failed to build.\n * They are loaded here, when an endpoint that uses one is served, which is\n * also when the app declaring that endpoint has installed it: an auditor's\n * storage, a rate limit's store and an RLS policy's database come from these\n * packages.\n *\n * The `try` is load-bearing beyond the error it names: a bundler leaves an\n * import it cannot resolve to run time when the import is guarded, and fails\n * the build when it is not.\n */\n\n/** An endpoint uses a feature whose optional peer is not installed. */\nexport class OptionalPeerMissing extends Error {\n\tconstructor(\n\t\treadonly packageName: string,\n\t\treadonly feature: string,\n\t\tcause: unknown,\n\t) {\n\t\tsuper(\n\t\t\t`${feature} needs '${packageName}', which could not be loaded. ` +\n\t\t\t\t`Add it to the dependencies of the app that serves this endpoint.`,\n\t\t\t{ cause },\n\t\t);\n\t\tthis.name = 'OptionalPeerMissing';\n\t}\n}\n\n/** `@geekmidas/audit`, for an endpoint with an auditor. */\nexport async function loadAudit(): Promise<typeof import('@geekmidas/audit')> {\n\ttry {\n\t\treturn await import('@geekmidas/audit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/audit',\n\t\t\t'An endpoint with an auditor',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/rate-limit`, for an endpoint with a rate limit. */\nexport async function loadRateLimit(): Promise<\n\ttypeof import('@geekmidas/rate-limit')\n> {\n\ttry {\n\t\treturn await import('@geekmidas/rate-limit');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/rate-limit',\n\t\t\t'An endpoint with a rate limit',\n\t\t\tcause,\n\t\t);\n\t}\n}\n\n/** `@geekmidas/db/rls`, for an endpoint with row-level security. */\nexport async function loadRls(): Promise<typeof import('@geekmidas/db/rls')> {\n\ttry {\n\t\treturn await import('@geekmidas/db/rls');\n\t} catch (cause) {\n\t\tthrow new OptionalPeerMissing(\n\t\t\t'@geekmidas/db',\n\t\t\t'An endpoint with row-level security',\n\t\t\tcause,\n\t\t);\n\t}\n}\n","import type {\n\tAuditActor,\n\tAuditableAction,\n\tAuditor,\n\tAuditStorage,\n} from '@geekmidas/audit';\nimport type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceDiscovery } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { ActorExtractor, MappedAudit } from './audit';\nimport type { CookieFn, Endpoint, HeaderFn } from './Endpoint';\nimport { loadAudit } from './optionalPeers';\n\n/**\n * Process declarative audit definitions after successful endpoint execution.\n * Similar to publishConstructEvents for events.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param response - The handler response to generate audit payloads from\n * @param serviceDiscovery - Service discovery for registering audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context (session, headers, cookies, services)\n * @param existingAuditor - Optional existing auditor instance (e.g., from handler context).\n * If provided, uses this auditor (with its stored transaction).\n * If not provided, creates a new auditor.\n */\nexport async function processEndpointAudits<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tOutSchema extends StandardSchemaV1 | undefined = undefined,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tOutSchema,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction\n\t>,\n\tresponse: InferStandardSchema<OutSchema>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n\texistingAuditor?: Auditor<TAuditAction>,\n): Promise<void> {\n\ttry {\n\t\tconst audits = endpoint.audits as MappedAudit<TAuditAction, OutSchema>[];\n\n\t\t// If we have an existing auditor (from handler context), we need to flush\n\t\t// any manual audits it collected, even if there are no declarative audits\n\t\tconst hasExistingRecords =\n\t\t\texistingAuditor && existingAuditor.getRecords().length > 0;\n\n\t\t// Skip if no declarative audits and no existing records to flush\n\t\tif (!audits?.length && !hasExistingRecords) {\n\t\t\tlogger.debug('No audits to process');\n\t\t\treturn;\n\t\t}\n\n\t\t// If no auditor storage service and we have things to process, warn\n\t\tif (!endpoint.auditorStorageService) {\n\t\t\tif (hasExistingRecords || audits?.length) {\n\t\t\t\tlogger.warn('No auditor storage service available');\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Get or create auditor\n\t\tlet auditor: Auditor<TAuditAction>;\n\n\t\tif (existingAuditor) {\n\t\t\t// Use existing auditor (preserves stored transaction and manual audits)\n\t\t\tauditor = existingAuditor;\n\t\t\tlogger.debug('Using existing auditor from handler context');\n\t\t} else {\n\t\t\t// Create new auditor (backward compatibility)\n\t\t\tconst services = await serviceDiscovery.register([\n\t\t\t\tendpoint.auditorStorageService,\n\t\t\t]);\n\t\t\tconst storage = services[\n\t\t\t\tendpoint.auditorStorageService.serviceName\n\t\t\t] as AuditStorage;\n\n\t\t\t// Extract actor if configured\n\t\t\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\t\t\tif (endpoint.actorExtractor) {\n\t\t\t\ttry {\n\t\t\t\t\tactor = await (\n\t\t\t\t\t\tendpoint.actorExtractor as ActorExtractor<\n\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\tTSession,\n\t\t\t\t\t\t\tTLogger\n\t\t\t\t\t\t>\n\t\t\t\t\t)({\n\t\t\t\t\t\tservices: ctx.services as any,\n\t\t\t\t\t\tsession: ctx.session,\n\t\t\t\t\t\theader: ctx.header,\n\t\t\t\t\t\tcookie: ctx.cookie,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t\t\t\t// Continue with system actor\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { DefaultAuditor } = await loadAudit();\n\t\t\tauditor = new DefaultAuditor<TAuditAction>({\n\t\t\t\tactor,\n\t\t\t\tstorage,\n\t\t\t\tmetadata: {\n\t\t\t\t\tendpoint: endpoint.route,\n\t\t\t\t\tmethod: endpoint.method,\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\n\t\t// Process each declarative audit\n\t\tif (audits?.length) {\n\t\t\tfor (const audit of audits) {\n\t\t\t\tlogger.debug({ audit: audit.type }, 'Processing declarative audit');\n\n\t\t\t\t// Check when condition\n\t\t\t\tif (audit.when && !audit.when(response as any)) {\n\t\t\t\t\tlogger.debug(\n\t\t\t\t\t\t{ audit: audit.type },\n\t\t\t\t\t\t'Audit skipped due to when condition',\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Extract payload\n\t\t\t\tconst payload = audit.payload(response as any);\n\n\t\t\t\t// Extract entityId if configured\n\t\t\t\tconst entityId = audit.entityId?.(response as any);\n\n\t\t\t\t// Record the audit\n\t\t\t\tauditor.audit(audit.type as any, payload as any, {\n\t\t\t\t\ttable: audit.table,\n\t\t\t\t\tentityId,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Flush audits to storage\n\t\t// Note: If existingAuditor has a stored transaction (via setTransaction),\n\t\t// flush() will use it automatically\n\t\tconst recordCount = auditor.getRecords().length;\n\t\tif (recordCount > 0) {\n\t\t\t// Check if auditor has a stored transaction (for logging purposes)\n\t\t\tconst trx =\n\t\t\t\t'getTransaction' in auditor\n\t\t\t\t\t? (auditor as { getTransaction(): unknown }).getTransaction()\n\t\t\t\t\t: undefined;\n\t\t\tlogger.debug(\n\t\t\t\t{ auditCount: recordCount, hasTransaction: !!trx },\n\t\t\t\t'Flushing audits',\n\t\t\t);\n\t\t\tawait auditor.flush();\n\t\t}\n\t} catch (error) {\n\t\tlogger.error(error as Error, 'Failed to process audits');\n\t\t// Don't rethrow - audit failures shouldn't fail the request\n\t}\n}\n\n/**\n * Context for audit-aware handler execution.\n */\nexport interface AuditExecutionContext<\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n> {\n\t/** The auditor instance for recording audits */\n\tauditor: Auditor<TAuditAction>;\n\t/** The audit storage instance */\n\tstorage: AuditStorage;\n}\n\n/**\n * Create audit context for handler execution.\n * Returns the auditor and storage for use in the handler.\n *\n * @param endpoint - The endpoint with audit configuration\n * @param serviceDiscovery - Service discovery for getting audit storage\n * @param logger - Logger for debug/error messages\n * @param ctx - Request context for actor extraction\n * @returns Audit context with auditor and storage, or undefined if not configured\n */\nexport async function createAuditContext<\n\tTServices extends Service[] = [],\n\tTSession = unknown,\n\tTLogger extends Logger = Logger,\n\tTAuditStorage extends AuditStorage | undefined = undefined,\n\tTAuditStorageServiceName extends string = string,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n\tTDatabase = undefined,\n\tTDatabaseServiceName extends string = string,\n>(\n\tendpoint: Endpoint<\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tany,\n\t\tTServices,\n\t\tTLogger,\n\t\tTSession,\n\t\tTAuditStorage,\n\t\tTAuditStorageServiceName,\n\t\tTAuditAction,\n\t\tTDatabase,\n\t\tTDatabaseServiceName\n\t>,\n\tserviceDiscovery: ServiceDiscovery<any>,\n\tlogger: TLogger,\n\tctx: {\n\t\tsession: TSession;\n\t\theader: HeaderFn;\n\t\tcookie: CookieFn;\n\t\tservices: Record<string, unknown>;\n\t},\n): Promise<AuditExecutionContext<TAuditAction> | undefined> {\n\tif (!endpoint.auditorStorageService) {\n\t\treturn undefined;\n\t}\n\n\tconst services = await serviceDiscovery.register([\n\t\tendpoint.auditorStorageService,\n\t]);\n\tconst storage = services[\n\t\tendpoint.auditorStorageService.serviceName\n\t] as AuditStorage;\n\n\t// Extract actor if configured\n\tlet actor: AuditActor = { id: 'system', type: 'system' };\n\tif (endpoint.actorExtractor) {\n\t\ttry {\n\t\t\tactor = await (\n\t\t\t\tendpoint.actorExtractor as ActorExtractor<TServices, TSession, TLogger>\n\t\t\t)({\n\t\t\t\tservices: ctx.services as any,\n\t\t\t\tsession: ctx.session,\n\t\t\t\theader: ctx.header,\n\t\t\t\tcookie: ctx.cookie,\n\t\t\t\tlogger,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tlogger.error(error as Error, 'Failed to extract actor for audits');\n\t\t}\n\t}\n\n\tconst { DefaultAuditor } = await loadAudit();\n\tconst auditor = new DefaultAuditor<TAuditAction>({\n\t\tactor,\n\t\tstorage,\n\t\tmetadata: {\n\t\t\tendpoint: endpoint.route,\n\t\t\tmethod: endpoint.method,\n\t\t},\n\t});\n\n\treturn { auditor, storage };\n}\n\n/**\n * Options for executeWithAuditTransaction.\n */\nexport interface ExecuteWithAuditTransactionOptions {\n\t/**\n\t * Database connection to use for the transaction.\n\t * If this is already a transaction, it will be reused instead of creating a nested one.\n\t * If not provided, the storage's internal database is used.\n\t */\n\tdb?: unknown;\n}\n\n/**\n * Execute a handler with automatic audit transaction support.\n * If the audit storage provides a withTransaction method, wraps execution\n * in a transaction so audits are atomic with handler's database operations.\n *\n * This is database-agnostic - each storage implementation provides its own\n * transaction handling based on the underlying database (Kysely, Drizzle, etc.).\n *\n * If the db parameter is provided and is already a transaction, the storage\n * will reuse it instead of creating a nested transaction (similar to\n * packages/db/src/kysely.ts#withTransaction).\n *\n * @param auditContext - The audit context from createAuditContext\n * @param handler - The handler function to execute (receives auditor)\n * @param onComplete - Called after handler with response, to process declarative audits\n * @param options - Optional configuration including database connection\n * @returns The handler result\n */\nexport async function executeWithAuditTransaction<\n\tT,\n\tTAuditAction extends AuditableAction<string, unknown> = AuditableAction<\n\t\tstring,\n\t\tunknown\n\t>,\n>(\n\tauditContext: AuditExecutionContext<TAuditAction> | undefined,\n\thandler: (auditor?: Auditor<TAuditAction>) => Promise<T>,\n\tonComplete?: (response: T, auditor: Auditor<TAuditAction>) => Promise<void>,\n\toptions?: ExecuteWithAuditTransactionOptions,\n): Promise<T> {\n\t// No audit context - just run handler\n\tif (!auditContext) {\n\t\treturn handler(undefined);\n\t}\n\n\tconst { auditor, storage } = auditContext;\n\n\t// Check if storage provides a transaction wrapper\n\tif (storage.withTransaction) {\n\t\t// Wrap in transaction - audits are atomic with handler operations\n\t\t// The storage's withTransaction handles setTransaction and flush\n\t\t// Pass db so existing transactions are reused\n\t\treturn storage.withTransaction(\n\t\t\tauditor,\n\t\t\tasync () => {\n\t\t\t\tconst response = await handler(auditor);\n\n\t\t\t\t// Process declarative audits within the transaction\n\t\t\t\tif (onComplete) {\n\t\t\t\t\tawait onComplete(response, auditor);\n\t\t\t\t}\n\n\t\t\t\treturn response;\n\t\t\t},\n\t\t\toptions?.db,\n\t\t);\n\t}\n\n\t// No transaction support - run handler and flush audits after\n\tconst response = await handler(auditor);\n\n\tif (onComplete) {\n\t\tawait onComplete(response, auditor);\n\t}\n\n\t// Flush audits (no transaction)\n\tawait auditor.flush();\n\n\treturn response;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,IAAa,sBAAb,cAAyC,MAAM;CAEpC;CACA;CAFV,YACC,aACA,SACA,OACC;EACD,MACC,GAAG,QAAQ,UAAU,YAAY,iGAEjC,EAAE,MAAM,CACT;EARS,KAAA,cAAA;EACA,KAAA,UAAA;EAQT,KAAK,OAAO;CACb;AACD;;AAGA,eAAsB,YAAwD;CAC7E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,oBACA,+BACA,KACD;CACD;AACD;;AAGA,eAAsB,gBAEpB;CACD,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,yBACA,iCACA,KACD;CACD;AACD;;AAGA,eAAsB,UAAuD;CAC5E,IAAI;EACH,OAAO,MAAM,OAAO;CACrB,SAAS,OAAO;EACf,MAAM,IAAI,oBACT,iBACA,uCACA,KACD;CACD;AACD;;;;;;;;;;;;;ACwIA,eAAsB,mBAarB,UAcA,kBACA,QACA,KAM2D;CAC3D,IAAI,CAAC,SAAS,uBACb;CAMD,MAAM,WAAU,MAHO,iBAAiB,SAAS,CAChD,SAAS,qBACV,CAAC,EAAA,CAEA,SAAS,sBAAsB;CAIhC,IAAI,QAAoB;EAAE,IAAI;EAAU,MAAM;CAAS;CACvD,IAAI,SAAS,gBACZ,IAAI;EACH,QAAQ,MACP,SAAS,eACR;GACD,UAAU,IAAI;GACd,SAAS,IAAI;GACb,QAAQ,IAAI;GACZ,QAAQ,IAAI;GACZ;EACD,CAAC;CACF,SAAS,OAAO;EACf,OAAO,MAAM,OAAgB,oCAAoC;CAClE;CAGD,MAAM,EAAE,mBAAmB,MAAM,UAAU;CAU3C,OAAO;EAAE,SAAA,IATW,eAA6B;GAChD;GACA;GACA,UAAU;IACT,UAAU,SAAS;IACnB,QAAQ,SAAS;GAClB;EACD,CAEe;EAAG;CAAQ;AAC3B;;;;;;;;;;;;;;;;;;;AAgCA,eAAsB,4BAOrB,cACA,SACA,YACA,SACa;CAEb,IAAI,CAAC,cACJ,OAAO,QAAQ,KAAA,CAAS;CAGzB,MAAM,EAAE,SAAS,YAAY;CAG7B,IAAI,QAAQ,iBAIX,OAAO,QAAQ,gBACd,SACA,YAAY;EACX,MAAM,WAAW,MAAM,QAAQ,OAAO;EAGtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;EAGnC,OAAO;CACR,GACA,SAAS,EACV;CAID,MAAM,WAAW,MAAM,QAAQ,OAAO;CAEtC,IAAI,YACH,MAAM,WAAW,UAAU,OAAO;CAInC,MAAM,QAAQ,MAAM;CAEpB,OAAO;AACR"}
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_Queue = require("../Queue-D5jcw0mu.cjs");
3
- const require_QueueBuilder = require("../QueueBuilder-BFQn3OzZ.cjs");
2
+ const require_Queue = require("../Queue-umpX3683.cjs");
3
+ const require_QueueBuilder = require("../QueueBuilder-B9ClmXjX.cjs");
4
4
  exports.Queue = require_Queue.Queue;
5
5
  exports.QueueNeedsMessage = require_QueueBuilder.QueueNeedsMessage;
6
6
  exports.QueueNeedsName = require_QueueBuilder.QueueNeedsName;
@@ -1,3 +1,3 @@
1
- import { t as Queue } from "../Queue-hwPN-t31.mjs";
2
- import { n as QueueNeedsMessage, r as QueueNeedsName } from "../QueueBuilder-DCuYdjMw.mjs";
1
+ import { t as Queue } from "../Queue-CLRT_j5a.mjs";
2
+ import { n as QueueNeedsMessage, r as QueueNeedsName } from "../QueueBuilder-B0mCqq74.mjs";
3
3
  export { Queue, QueueNeedsMessage, QueueNeedsName };
package/dist/rest-api.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_construct_interface = require("./construct-interface.cjs");
3
- const require_EndpointFactory = require("./EndpointFactory-CLxrcS0O.cjs");
3
+ const require_EndpointFactory = require("./EndpointFactory-B_x2fXWQ.cjs");
4
4
  const require_surfaceEnv = require("./surfaceEnv-CNimK76p.cjs");
5
5
  let _geekmidas_manifest = require("@geekmidas/manifest");
6
6
  let _geekmidas_logger_console = require("@geekmidas/logger/console");
@@ -1,6 +1,6 @@
1
1
  import { i as Declarable, t as Authenticator } from "./construct-interface-B1AfqRl4.cjs";
2
- import { T as BuiltInSecuritySchemeId } from "./Endpoint-DV3jK5sP.cjs";
3
- import { t as EndpointFactory } from "./EndpointFactory-CLzCLkLt.cjs";
2
+ import { T as BuiltInSecuritySchemeId } from "./Endpoint-DJZkADYh.cjs";
3
+ import { t as EndpointFactory } from "./EndpointFactory-BkYuVg8W.cjs";
4
4
  import { EnvironmentParser } from "@geekmidas/envkit";
5
5
  import { Logger } from "@geekmidas/logger";
6
6
  import { ConstructName, Declaration, Dependency } from "@geekmidas/manifest";
@@ -1,6 +1,6 @@
1
1
  import { i as Declarable, t as Authenticator } from "./construct-interface-DluK3_da.mjs";
2
- import { T as BuiltInSecuritySchemeId } from "./Endpoint-BJDBZBZu.mjs";
3
- import { t as EndpointFactory } from "./EndpointFactory-gaF4yRpP.mjs";
2
+ import { T as BuiltInSecuritySchemeId } from "./Endpoint-hcHSFhda.mjs";
3
+ import { t as EndpointFactory } from "./EndpointFactory-B5siFRcO.mjs";
4
4
  import { ConstructName, Declaration, Dependency } from "@geekmidas/manifest";
5
5
  import { EnvironmentParser } from "@geekmidas/envkit";
6
6
  import { Logger } from "@geekmidas/logger";
package/dist/rest-api.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { edgeTo } from "./construct-interface.mjs";
2
- import { o as getSecurityScheme, t as EndpointFactory } from "./EndpointFactory-7g4Dg6cW.mjs";
2
+ import { o as getSecurityScheme, t as EndpointFactory } from "./EndpointFactory-C3QvXMk6.mjs";
3
3
  import { n as envParserFor } from "./surfaceEnv-wMT5QzgZ.mjs";
4
4
  import { canonicalId, provideKey } from "@geekmidas/manifest";
5
5
  import { DEFAULT_LOGGER } from "@geekmidas/logger/console";
@@ -21,7 +21,7 @@ function subscribedEvents(subscriber, rawEvent, logger) {
21
21
  if (isSubscribed(subscriber, event)) events.push(event);
22
22
  } catch (error) {
23
23
  logger.error({
24
- error,
24
+ err: error,
25
25
  record
26
26
  }, "Failed to parse record");
27
27
  }
@@ -106,4 +106,4 @@ function safeJsonParse(value) {
106
106
  //#endregion
107
107
  export { subscribedEvents as n, subscriberContext as r, runSubscriber as t };
108
108
 
109
- //# sourceMappingURL=runSubscriber-BYEY_bfA.mjs.map
109
+ //# sourceMappingURL=runSubscriber-BHvJ8POr.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runSubscriber-BHvJ8POr.mjs","names":[],"sources":["../src/subscribers/runSubscriber.ts"],"sourcesContent":["/**\n * What a subscriber does with a batch of records, however they arrived: parse\n * them into events, keep the ones it subscribed to, and run its handler with\n * its services and database.\n *\n * Shared by the Lambda adaptor, which wraps it in middy, and the SNS push\n * adaptor, which a server runs and which must not need middy to load.\n */\n\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport { ServiceDiscovery } from '@geekmidas/services';\nimport type { SNSEvent, SNSEventRecord, SQSEvent, SQSRecord } from 'aws-lambda';\nimport type { Subscriber } from './Subscriber';\n\ntype AnySubscriber = Subscriber<any, any, any, any, any, any, any>;\n\n/** The subscriber's output did not match its `.output()` schema. */\nexport class SubscriberOutputInvalid extends Error {\n\tconstructor(readonly issues: ReadonlyArray<unknown>) {\n\t\tsuper(\n\t\t\t\"Subscriber output validation failed: the handler's result does not match its .output() schema.\",\n\t\t);\n\t\tthis.name = 'SubscriberOutputInvalid';\n\t}\n}\n\n/** The events in an SQS or SNS batch (a Lambda event's `Records`) that `subscriber` subscribed to. */\nexport function subscribedEvents(\n\tsubscriber: AnySubscriber,\n\trawEvent: { Records?: readonly unknown[] },\n\tlogger: Logger,\n): any[] {\n\tconst events: any[] = [];\n\tconst records = rawEvent.Records ?? [];\n\tconst first = records[0] as any;\n\tconst parse =\n\t\tfirst?.eventSource === 'aws:sqs'\n\t\t\t? (record: any) => parseSQSRecord(record)\n\t\t\t: first?.EventSource === 'aws:sns'\n\t\t\t\t? (record: any) => parseSNSRecord(record)\n\t\t\t\t: undefined;\n\tif (!parse) return events;\n\n\tfor (const record of records) {\n\t\ttry {\n\t\t\tconst event = parse(record);\n\t\t\tif (isSubscribed(subscriber, event)) events.push(event);\n\t\t} catch (error) {\n\t\t\tlogger.error({ err: error, record }, 'Failed to parse record');\n\t\t}\n\t}\n\treturn events;\n}\n\n/** The subscriber's services and database, registered as functions do. */\nexport async function subscriberContext(\n\tsubscriber: AnySubscriber,\n\tenvParser: EnvironmentParser<{}>,\n): Promise<{ services: Record<string, unknown>; db: unknown }> {\n\tconst discovery = ServiceDiscovery.getInstance(envParser);\n\tconst services =\n\t\tsubscriber.services.length > 0\n\t\t\t? await discovery.register(subscriber.services)\n\t\t\t: {};\n\tconst service = subscriber.databaseService;\n\tconst db = service\n\t\t? (await discovery.register([service]))[service.serviceName]\n\t\t: undefined;\n\treturn { services, db };\n}\n\n/** Runs the handler on `events`, and checks its output against its schema. */\nexport async function runSubscriber(\n\tsubscriber: AnySubscriber,\n\tcontext: {\n\t\tevents: any[];\n\t\tservices: Record<string, unknown>;\n\t\tdb: unknown;\n\t\tlogger: Logger;\n\t},\n): Promise<unknown> {\n\tif (context.events.length === 0) {\n\t\tcontext.logger.info('No subscribed events to process');\n\t\treturn { batchItemFailures: [] };\n\t}\n\n\tconst { events, services, db, logger } = context;\n\tconst result = await subscriber.handler({\n\t\tevents,\n\t\tservices,\n\t\tlogger,\n\t\tdb,\n\t} as any);\n\n\tif (subscriber.outputSchema && result) {\n\t\tconst validation =\n\t\t\tawait subscriber.outputSchema['~standard'].validate(result);\n\t\tif (validation.issues) {\n\t\t\tcontext.logger.error(\n\t\t\t\t{ issues: validation.issues },\n\t\t\t\t'Subscriber output validation failed',\n\t\t\t);\n\t\t\tthrow new SubscriberOutputInvalid(validation.issues);\n\t\t}\n\t\treturn validation.value;\n\t}\n\n\treturn result;\n}\n\nfunction isSubscribed(subscriber: AnySubscriber, event: any): boolean {\n\t// No event type (raw string/non-object) — always include\n\tif (typeof event !== 'object' || !event?.type) return true;\n\t// No filter configured — accept all\n\tif (!subscriber.subscribedEvents) return true;\n\treturn subscriber.subscribedEvents.includes(event.type);\n}\n\nfunction parseSNSRecord(record: SNSEventRecord): any {\n\tconst message = safeJsonParse(record.Sns.Message);\n\tconst messageType = record.Sns.MessageAttributes?.type?.Value;\n\n\t// Not JSON — wrap raw string with type from MessageAttributes if available\n\tif (message === null) {\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: record.Sns.Message }\n\t\t\t: record.Sns.Message;\n\t}\n\n\tif (message.type) {\n\t\treturn message; // Full event format: { type, payload }\n\t}\n\n\t// Payload-only format: type is in MessageAttributes\n\treturn messageType ? { type: messageType, payload: message } : message;\n}\n\nfunction parseSQSRecord(record: SQSRecord): any {\n\tconst body = safeJsonParse(record.body);\n\n\t// Not JSON — return raw body as-is\n\tif (body === null) {\n\t\treturn record.body;\n\t}\n\n\t// Check if this is an SNS message wrapped in SQS\n\tif (body.Type === 'Notification' && body.Message) {\n\t\tconst snsMessage = safeJsonParse(body.Message);\n\t\tconst messageType = body.MessageAttributes?.type?.Value;\n\n\t\t// SNS Message not JSON — wrap with type from MessageAttributes if available\n\t\tif (snsMessage === null) {\n\t\t\treturn messageType\n\t\t\t\t? { type: messageType, payload: body.Message }\n\t\t\t\t: body.Message;\n\t\t}\n\n\t\tif (snsMessage.type) {\n\t\t\treturn snsMessage; // Full event format: { type, payload }\n\t\t}\n\n\t\t// Payload-only format: type is in MessageAttributes\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: snsMessage }\n\t\t\t: snsMessage;\n\t}\n\n\t// Direct SQS message\n\treturn body;\n}\n\nfunction safeJsonParse(value: string): any | null {\n\ttry {\n\t\treturn JSON.parse(value);\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
@@ -21,7 +21,7 @@ function subscribedEvents(subscriber, rawEvent, logger) {
21
21
  if (isSubscribed(subscriber, event)) events.push(event);
22
22
  } catch (error) {
23
23
  logger.error({
24
- error,
24
+ err: error,
25
25
  record
26
26
  }, "Failed to parse record");
27
27
  }
@@ -123,4 +123,4 @@ Object.defineProperty(exports, "subscriberContext", {
123
123
  }
124
124
  });
125
125
 
126
- //# sourceMappingURL=runSubscriber-BRp9a_qi.cjs.map
126
+ //# sourceMappingURL=runSubscriber-D8nLXIv1.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runSubscriber-D8nLXIv1.cjs","names":["ServiceDiscovery"],"sources":["../src/subscribers/runSubscriber.ts"],"sourcesContent":["/**\n * What a subscriber does with a batch of records, however they arrived: parse\n * them into events, keep the ones it subscribed to, and run its handler with\n * its services and database.\n *\n * Shared by the Lambda adaptor, which wraps it in middy, and the SNS push\n * adaptor, which a server runs and which must not need middy to load.\n */\n\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport { ServiceDiscovery } from '@geekmidas/services';\nimport type { SNSEvent, SNSEventRecord, SQSEvent, SQSRecord } from 'aws-lambda';\nimport type { Subscriber } from './Subscriber';\n\ntype AnySubscriber = Subscriber<any, any, any, any, any, any, any>;\n\n/** The subscriber's output did not match its `.output()` schema. */\nexport class SubscriberOutputInvalid extends Error {\n\tconstructor(readonly issues: ReadonlyArray<unknown>) {\n\t\tsuper(\n\t\t\t\"Subscriber output validation failed: the handler's result does not match its .output() schema.\",\n\t\t);\n\t\tthis.name = 'SubscriberOutputInvalid';\n\t}\n}\n\n/** The events in an SQS or SNS batch (a Lambda event's `Records`) that `subscriber` subscribed to. */\nexport function subscribedEvents(\n\tsubscriber: AnySubscriber,\n\trawEvent: { Records?: readonly unknown[] },\n\tlogger: Logger,\n): any[] {\n\tconst events: any[] = [];\n\tconst records = rawEvent.Records ?? [];\n\tconst first = records[0] as any;\n\tconst parse =\n\t\tfirst?.eventSource === 'aws:sqs'\n\t\t\t? (record: any) => parseSQSRecord(record)\n\t\t\t: first?.EventSource === 'aws:sns'\n\t\t\t\t? (record: any) => parseSNSRecord(record)\n\t\t\t\t: undefined;\n\tif (!parse) return events;\n\n\tfor (const record of records) {\n\t\ttry {\n\t\t\tconst event = parse(record);\n\t\t\tif (isSubscribed(subscriber, event)) events.push(event);\n\t\t} catch (error) {\n\t\t\tlogger.error({ err: error, record }, 'Failed to parse record');\n\t\t}\n\t}\n\treturn events;\n}\n\n/** The subscriber's services and database, registered as functions do. */\nexport async function subscriberContext(\n\tsubscriber: AnySubscriber,\n\tenvParser: EnvironmentParser<{}>,\n): Promise<{ services: Record<string, unknown>; db: unknown }> {\n\tconst discovery = ServiceDiscovery.getInstance(envParser);\n\tconst services =\n\t\tsubscriber.services.length > 0\n\t\t\t? await discovery.register(subscriber.services)\n\t\t\t: {};\n\tconst service = subscriber.databaseService;\n\tconst db = service\n\t\t? (await discovery.register([service]))[service.serviceName]\n\t\t: undefined;\n\treturn { services, db };\n}\n\n/** Runs the handler on `events`, and checks its output against its schema. */\nexport async function runSubscriber(\n\tsubscriber: AnySubscriber,\n\tcontext: {\n\t\tevents: any[];\n\t\tservices: Record<string, unknown>;\n\t\tdb: unknown;\n\t\tlogger: Logger;\n\t},\n): Promise<unknown> {\n\tif (context.events.length === 0) {\n\t\tcontext.logger.info('No subscribed events to process');\n\t\treturn { batchItemFailures: [] };\n\t}\n\n\tconst { events, services, db, logger } = context;\n\tconst result = await subscriber.handler({\n\t\tevents,\n\t\tservices,\n\t\tlogger,\n\t\tdb,\n\t} as any);\n\n\tif (subscriber.outputSchema && result) {\n\t\tconst validation =\n\t\t\tawait subscriber.outputSchema['~standard'].validate(result);\n\t\tif (validation.issues) {\n\t\t\tcontext.logger.error(\n\t\t\t\t{ issues: validation.issues },\n\t\t\t\t'Subscriber output validation failed',\n\t\t\t);\n\t\t\tthrow new SubscriberOutputInvalid(validation.issues);\n\t\t}\n\t\treturn validation.value;\n\t}\n\n\treturn result;\n}\n\nfunction isSubscribed(subscriber: AnySubscriber, event: any): boolean {\n\t// No event type (raw string/non-object) — always include\n\tif (typeof event !== 'object' || !event?.type) return true;\n\t// No filter configured — accept all\n\tif (!subscriber.subscribedEvents) return true;\n\treturn subscriber.subscribedEvents.includes(event.type);\n}\n\nfunction parseSNSRecord(record: SNSEventRecord): any {\n\tconst message = safeJsonParse(record.Sns.Message);\n\tconst messageType = record.Sns.MessageAttributes?.type?.Value;\n\n\t// Not JSON — wrap raw string with type from MessageAttributes if available\n\tif (message === null) {\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: record.Sns.Message }\n\t\t\t: record.Sns.Message;\n\t}\n\n\tif (message.type) {\n\t\treturn message; // Full event format: { type, payload }\n\t}\n\n\t// Payload-only format: type is in MessageAttributes\n\treturn messageType ? { type: messageType, payload: message } : message;\n}\n\nfunction parseSQSRecord(record: SQSRecord): any {\n\tconst body = safeJsonParse(record.body);\n\n\t// Not JSON — return raw body as-is\n\tif (body === null) {\n\t\treturn record.body;\n\t}\n\n\t// Check if this is an SNS message wrapped in SQS\n\tif (body.Type === 'Notification' && body.Message) {\n\t\tconst snsMessage = safeJsonParse(body.Message);\n\t\tconst messageType = body.MessageAttributes?.type?.Value;\n\n\t\t// SNS Message not JSON — wrap with type from MessageAttributes if available\n\t\tif (snsMessage === null) {\n\t\t\treturn messageType\n\t\t\t\t? { type: messageType, payload: body.Message }\n\t\t\t\t: body.Message;\n\t\t}\n\n\t\tif (snsMessage.type) {\n\t\t\treturn snsMessage; // Full event format: { type, payload }\n\t\t}\n\n\t\t// Payload-only format: type is in MessageAttributes\n\t\treturn messageType\n\t\t\t? { type: messageType, payload: snsMessage }\n\t\t\t: snsMessage;\n\t}\n\n\t// Direct SQS message\n\treturn body;\n}\n\nfunction safeJsonParse(value: string): any | null {\n\ttry {\n\t\treturn JSON.parse(value);\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_SubscriberBuilder = require("../SubscriberBuilder-C2PwqwIl.cjs");
3
- const require_runSubscriber = require("../runSubscriber-BRp9a_qi.cjs");
2
+ const require_SubscriberBuilder = require("../SubscriberBuilder-D_owVoHo.cjs");
3
+ const require_runSubscriber = require("../runSubscriber-D8nLXIv1.cjs");
4
4
  let _geekmidas_errors = require("@geekmidas/errors");
5
5
  let _geekmidas_services = require("@geekmidas/services");
6
6
  let _geekmidas_events_sns = require("@geekmidas/events/sns");
@@ -31,7 +31,7 @@ var SnsPushSubscriberAdaptor = class {
31
31
  await (0, _geekmidas_events_sns.verifySnsMessage)(message);
32
32
  } catch (error) {
33
33
  this.subscriber.logger.warn({
34
- error,
34
+ err: error,
35
35
  topicArn: message.TopicArn
36
36
  }, "Refused an SNS message that did not verify");
37
37
  return refuse(403, error.name);
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["verifySnsMessage","confirmSnsSubscription","runWithRequestContext","subscriberContext","subscribedEvents","toSnsEvent","runSubscriber","wrapError"],"sources":["../../src/subscribers/SnsPushSubscriberAdaptor.ts"],"sourcesContent":["import type { EnvironmentParser } from '@geekmidas/envkit';\nimport { wrapError } from '@geekmidas/errors';\nimport {\n\tconfirmSnsSubscription,\n\ttype SnsHttpMessage,\n\ttoSnsEvent,\n\tverifySnsMessage,\n} from '@geekmidas/events/sns';\nimport { runWithRequestContext } from '@geekmidas/services';\nimport {\n\trunSubscriber,\n\tsubscribedEvents,\n\tsubscriberContext,\n} from './runSubscriber';\nimport type { Subscriber } from './Subscriber';\n\nexport interface SnsPushOptions {\n\t/** The topic this subscriber is subscribed to. Anything else is refused. */\n\ttopicArn: string;\n\t/**\n\t * Verify each message's signature. Off only against an emulator, which\n\t * signs nothing — decided by the connection string's endpoint, never by\n\t * anything the request says about itself.\n\t */\n\tverify: boolean;\n\t/**\n\t * The emulator's address, as this server was configured with it. An\n\t * emulator names itself in `SubscribeURL` by the port it listens on inside\n\t * its container, which is not the port published to the host — so against\n\t * one, the confirmation is sent here instead. From configuration, never\n\t * from the request.\n\t */\n\temulatorEndpoint?: string;\n}\n\n/** What to answer SNS with. A non-2xx tells SNS to retry. */\nexport interface SnsPushResponse {\n\tstatus: 200 | 400 | 403 | 500;\n\tbody: { status: string; error?: string };\n}\n\n/**\n * A topic subscriber run by SNS pushing to an HTTP endpoint, rather than by a\n * poller.\n *\n * The notification is read as the Lambda event SNS would have invoked a\n * function with, and run through the same parsing, services and handler as the\n * Lambda adaptor — without its middy wrapper, which is Lambda's alone: a\n * server serving this route has no reason to install `@middy/core`.\n */\nexport class SnsPushSubscriberAdaptor {\n\tconstructor(\n\t\tprivate readonly envParser: EnvironmentParser<{}>,\n\t\treadonly subscriber: Subscriber<any, any, any, any, any, any, any>,\n\t\tprivate readonly options: SnsPushOptions,\n\t) {}\n\n\tasync handle(body: unknown): Promise<SnsPushResponse> {\n\t\tconst message = body as SnsHttpMessage;\n\t\tif (typeof message?.Type !== 'string' || !message.TopicArn) {\n\t\t\treturn refuse(400, 'not an SNS message');\n\t\t}\n\t\t// Checked before the signature: a genuine message for another topic is\n\t\t// still not this subscriber's to confirm or to handle.\n\t\tif (message.TopicArn !== this.options.topicArn) {\n\t\t\treturn refuse(403, `not subscribed to ${message.TopicArn}`);\n\t\t}\n\n\t\tif (this.options.verify) {\n\t\t\ttry {\n\t\t\t\tawait verifySnsMessage(message);\n\t\t\t} catch (error) {\n\t\t\t\tthis.subscriber.logger.warn(\n\t\t\t\t\t{ error, topicArn: message.TopicArn },\n\t\t\t\t\t'Refused an SNS message that did not verify',\n\t\t\t\t);\n\t\t\t\treturn refuse(403, (error as Error).name);\n\t\t\t}\n\t\t}\n\n\t\tswitch (message.Type) {\n\t\t\tcase 'SubscriptionConfirmation':\n\t\t\t\tawait confirmSnsSubscription(\n\t\t\t\t\tthis.options.emulatorEndpoint && message.SubscribeURL\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t...message,\n\t\t\t\t\t\t\t\tSubscribeURL: throughEndpoint(\n\t\t\t\t\t\t\t\t\tmessage.SubscribeURL,\n\t\t\t\t\t\t\t\t\tthis.options.emulatorEndpoint,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: message,\n\t\t\t\t);\n\t\t\t\tthis.subscriber.logger.info(\n\t\t\t\t\t{ topicArn: message.TopicArn },\n\t\t\t\t\t'Confirmed SNS subscription',\n\t\t\t\t);\n\t\t\t\treturn { status: 200, body: { status: 'confirmed' } };\n\t\t\tcase 'Notification':\n\t\t\t\ttry {\n\t\t\t\t\tawait this.notify(message);\n\t\t\t\t\treturn { status: 200, body: { status: 'ok' } };\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Already logged. A 500 makes SNS retry, which is the point of\n\t\t\t\t\t// answering at all.\n\t\t\t\t\treturn refuse(500, (error as Error).name);\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn { status: 200, body: { status: 'ignored' } };\n\t\t}\n\t}\n\n\tprivate async notify(message: SnsHttpMessage): Promise<void> {\n\t\tconst requestId = message.MessageId;\n\t\tconst logger = this.subscriber.logger.child({ requestId });\n\n\t\tawait runWithRequestContext(\n\t\t\t{\n\t\t\t\tlogger,\n\t\t\t\trequestId,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t\toperation: `subscriber ${this.subscriber.topicName ?? 'sns'}`,\n\t\t\t},\n\t\t\tasync () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst { services, db } = await subscriberContext(\n\t\t\t\t\t\tthis.subscriber,\n\t\t\t\t\t\tthis.envParser,\n\t\t\t\t\t);\n\t\t\t\t\tconst events = subscribedEvents(\n\t\t\t\t\t\tthis.subscriber,\n\t\t\t\t\t\ttoSnsEvent(message),\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t);\n\t\t\t\t\tawait runSubscriber(this.subscriber, {\n\t\t\t\t\t\tevents,\n\t\t\t\t\t\tservices,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t\tdb,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlogger.error(error as object, 'Error processing subscriber');\n\t\t\t\t\tthrow wrapError(error);\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\t}\n}\n\n/** `url`, sent to `endpoint`'s origin instead of its own. */\nfunction throughEndpoint(url: string, endpoint: string): string {\n\tconst target = new URL(url);\n\tconst origin = new URL(endpoint);\n\ttarget.protocol = origin.protocol;\n\ttarget.host = origin.host;\n\treturn target.toString();\n}\n\nfunction refuse(\n\tstatus: SnsPushResponse['status'],\n\terror: string,\n): SnsPushResponse {\n\treturn { status, body: { status: 'refused', error } };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkDA,IAAa,2BAAb,MAAsC;CAEnB;CACR;CACQ;CAHlB,YACC,WACA,YACA,SACC;EAHgB,KAAA,YAAA;EACR,KAAA,aAAA;EACQ,KAAA,UAAA;CACf;CAEH,MAAM,OAAO,MAAyC;EACrD,MAAM,UAAU;EAChB,IAAI,OAAO,SAAS,SAAS,YAAY,CAAC,QAAQ,UACjD,OAAO,OAAO,KAAK,oBAAoB;EAIxC,IAAI,QAAQ,aAAa,KAAK,QAAQ,UACrC,OAAO,OAAO,KAAK,qBAAqB,QAAQ,UAAU;EAG3D,IAAI,KAAK,QAAQ,QAChB,IAAI;GACH,OAAA,GAAMA,sBAAAA,iBAAAA,CAAiB,OAAO;EAC/B,SAAS,OAAO;GACf,KAAK,WAAW,OAAO,KACtB;IAAE;IAAO,UAAU,QAAQ;GAAS,GACpC,4CACD;GACA,OAAO,OAAO,KAAM,MAAgB,IAAI;EACzC;EAGD,QAAQ,QAAQ,MAAhB;GACC,KAAK;IACJ,OAAA,GAAMC,sBAAAA,uBAAAA,CACL,KAAK,QAAQ,oBAAoB,QAAQ,eACtC;KACA,GAAG;KACH,cAAc,gBACb,QAAQ,cACR,KAAK,QAAQ,gBACd;IACD,IACC,OACJ;IACA,KAAK,WAAW,OAAO,KACtB,EAAE,UAAU,QAAQ,SAAS,GAC7B,4BACD;IACA,OAAO;KAAE,QAAQ;KAAK,MAAM,EAAE,QAAQ,YAAY;IAAE;GACrD,KAAK,gBACJ,IAAI;IACH,MAAM,KAAK,OAAO,OAAO;IACzB,OAAO;KAAE,QAAQ;KAAK,MAAM,EAAE,QAAQ,KAAK;IAAE;GAC9C,SAAS,OAAO;IAGf,OAAO,OAAO,KAAM,MAAgB,IAAI;GACzC;GACD,SACC,OAAO;IAAE,QAAQ;IAAK,MAAM,EAAE,QAAQ,UAAU;GAAE;EACpD;CACD;CAEA,MAAc,OAAO,SAAwC;EAC5D,MAAM,YAAY,QAAQ;EAC1B,MAAM,SAAS,KAAK,WAAW,OAAO,MAAM,EAAE,UAAU,CAAC;EAEzD,OAAA,GAAMC,oBAAAA,sBAAAA,CACL;GACC;GACA;GACA,WAAW,KAAK,IAAI;GACpB,WAAW,cAAc,KAAK,WAAW,aAAa;EACvD,GACA,YAAY;GACX,IAAI;IACH,MAAM,EAAE,UAAU,OAAO,MAAMC,sBAAAA,kBAC9B,KAAK,YACL,KAAK,SACN;IACA,MAAM,SAASC,sBAAAA,iBACd,KAAK,aAAA,GACLC,sBAAAA,WAAAA,CAAW,OAAO,GAClB,MACD;IACA,MAAMC,sBAAAA,cAAc,KAAK,YAAY;KACpC;KACA;KACA;KACA;IACD,CAAC;GACF,SAAS,OAAO;IACf,OAAO,MAAM,OAAiB,6BAA6B;IAC3D,OAAA,GAAMC,kBAAAA,UAAAA,CAAU,KAAK;GACtB;EACD,CACD;CACD;AACD;;AAGA,SAAS,gBAAgB,KAAa,UAA0B;CAC/D,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,SAAS,IAAI,IAAI,QAAQ;CAC/B,OAAO,WAAW,OAAO;CACzB,OAAO,OAAO,OAAO;CACrB,OAAO,OAAO,SAAS;AACxB;AAEA,SAAS,OACR,QACA,OACkB;CAClB,OAAO;EAAE;EAAQ,MAAM;GAAE,QAAQ;GAAW;EAAM;CAAE;AACrD"}
1
+ {"version":3,"file":"index.cjs","names":["verifySnsMessage","confirmSnsSubscription","runWithRequestContext","subscriberContext","subscribedEvents","toSnsEvent","runSubscriber","wrapError"],"sources":["../../src/subscribers/SnsPushSubscriberAdaptor.ts"],"sourcesContent":["import type { EnvironmentParser } from '@geekmidas/envkit';\nimport { wrapError } from '@geekmidas/errors';\nimport {\n\tconfirmSnsSubscription,\n\ttype SnsHttpMessage,\n\ttoSnsEvent,\n\tverifySnsMessage,\n} from '@geekmidas/events/sns';\nimport { runWithRequestContext } from '@geekmidas/services';\nimport {\n\trunSubscriber,\n\tsubscribedEvents,\n\tsubscriberContext,\n} from './runSubscriber';\nimport type { Subscriber } from './Subscriber';\n\nexport interface SnsPushOptions {\n\t/** The topic this subscriber is subscribed to. Anything else is refused. */\n\ttopicArn: string;\n\t/**\n\t * Verify each message's signature. Off only against an emulator, which\n\t * signs nothing — decided by the connection string's endpoint, never by\n\t * anything the request says about itself.\n\t */\n\tverify: boolean;\n\t/**\n\t * The emulator's address, as this server was configured with it. An\n\t * emulator names itself in `SubscribeURL` by the port it listens on inside\n\t * its container, which is not the port published to the host — so against\n\t * one, the confirmation is sent here instead. From configuration, never\n\t * from the request.\n\t */\n\temulatorEndpoint?: string;\n}\n\n/** What to answer SNS with. A non-2xx tells SNS to retry. */\nexport interface SnsPushResponse {\n\tstatus: 200 | 400 | 403 | 500;\n\tbody: { status: string; error?: string };\n}\n\n/**\n * A topic subscriber run by SNS pushing to an HTTP endpoint, rather than by a\n * poller.\n *\n * The notification is read as the Lambda event SNS would have invoked a\n * function with, and run through the same parsing, services and handler as the\n * Lambda adaptor — without its middy wrapper, which is Lambda's alone: a\n * server serving this route has no reason to install `@middy/core`.\n */\nexport class SnsPushSubscriberAdaptor {\n\tconstructor(\n\t\tprivate readonly envParser: EnvironmentParser<{}>,\n\t\treadonly subscriber: Subscriber<any, any, any, any, any, any, any>,\n\t\tprivate readonly options: SnsPushOptions,\n\t) {}\n\n\tasync handle(body: unknown): Promise<SnsPushResponse> {\n\t\tconst message = body as SnsHttpMessage;\n\t\tif (typeof message?.Type !== 'string' || !message.TopicArn) {\n\t\t\treturn refuse(400, 'not an SNS message');\n\t\t}\n\t\t// Checked before the signature: a genuine message for another topic is\n\t\t// still not this subscriber's to confirm or to handle.\n\t\tif (message.TopicArn !== this.options.topicArn) {\n\t\t\treturn refuse(403, `not subscribed to ${message.TopicArn}`);\n\t\t}\n\n\t\tif (this.options.verify) {\n\t\t\ttry {\n\t\t\t\tawait verifySnsMessage(message);\n\t\t\t} catch (error) {\n\t\t\t\tthis.subscriber.logger.warn(\n\t\t\t\t\t{ err: error, topicArn: message.TopicArn },\n\t\t\t\t\t'Refused an SNS message that did not verify',\n\t\t\t\t);\n\t\t\t\treturn refuse(403, (error as Error).name);\n\t\t\t}\n\t\t}\n\n\t\tswitch (message.Type) {\n\t\t\tcase 'SubscriptionConfirmation':\n\t\t\t\tawait confirmSnsSubscription(\n\t\t\t\t\tthis.options.emulatorEndpoint && message.SubscribeURL\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t...message,\n\t\t\t\t\t\t\t\tSubscribeURL: throughEndpoint(\n\t\t\t\t\t\t\t\t\tmessage.SubscribeURL,\n\t\t\t\t\t\t\t\t\tthis.options.emulatorEndpoint,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: message,\n\t\t\t\t);\n\t\t\t\tthis.subscriber.logger.info(\n\t\t\t\t\t{ topicArn: message.TopicArn },\n\t\t\t\t\t'Confirmed SNS subscription',\n\t\t\t\t);\n\t\t\t\treturn { status: 200, body: { status: 'confirmed' } };\n\t\t\tcase 'Notification':\n\t\t\t\ttry {\n\t\t\t\t\tawait this.notify(message);\n\t\t\t\t\treturn { status: 200, body: { status: 'ok' } };\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Already logged. A 500 makes SNS retry, which is the point of\n\t\t\t\t\t// answering at all.\n\t\t\t\t\treturn refuse(500, (error as Error).name);\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn { status: 200, body: { status: 'ignored' } };\n\t\t}\n\t}\n\n\tprivate async notify(message: SnsHttpMessage): Promise<void> {\n\t\tconst requestId = message.MessageId;\n\t\tconst logger = this.subscriber.logger.child({ requestId });\n\n\t\tawait runWithRequestContext(\n\t\t\t{\n\t\t\t\tlogger,\n\t\t\t\trequestId,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t\toperation: `subscriber ${this.subscriber.topicName ?? 'sns'}`,\n\t\t\t},\n\t\t\tasync () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst { services, db } = await subscriberContext(\n\t\t\t\t\t\tthis.subscriber,\n\t\t\t\t\t\tthis.envParser,\n\t\t\t\t\t);\n\t\t\t\t\tconst events = subscribedEvents(\n\t\t\t\t\t\tthis.subscriber,\n\t\t\t\t\t\ttoSnsEvent(message),\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t);\n\t\t\t\t\tawait runSubscriber(this.subscriber, {\n\t\t\t\t\t\tevents,\n\t\t\t\t\t\tservices,\n\t\t\t\t\t\tlogger,\n\t\t\t\t\t\tdb,\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tlogger.error(error as object, 'Error processing subscriber');\n\t\t\t\t\tthrow wrapError(error);\n\t\t\t\t}\n\t\t\t},\n\t\t);\n\t}\n}\n\n/** `url`, sent to `endpoint`'s origin instead of its own. */\nfunction throughEndpoint(url: string, endpoint: string): string {\n\tconst target = new URL(url);\n\tconst origin = new URL(endpoint);\n\ttarget.protocol = origin.protocol;\n\ttarget.host = origin.host;\n\treturn target.toString();\n}\n\nfunction refuse(\n\tstatus: SnsPushResponse['status'],\n\terror: string,\n): SnsPushResponse {\n\treturn { status, body: { status: 'refused', error } };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAkDA,IAAa,2BAAb,MAAsC;CAEnB;CACR;CACQ;CAHlB,YACC,WACA,YACA,SACC;EAHgB,KAAA,YAAA;EACR,KAAA,aAAA;EACQ,KAAA,UAAA;CACf;CAEH,MAAM,OAAO,MAAyC;EACrD,MAAM,UAAU;EAChB,IAAI,OAAO,SAAS,SAAS,YAAY,CAAC,QAAQ,UACjD,OAAO,OAAO,KAAK,oBAAoB;EAIxC,IAAI,QAAQ,aAAa,KAAK,QAAQ,UACrC,OAAO,OAAO,KAAK,qBAAqB,QAAQ,UAAU;EAG3D,IAAI,KAAK,QAAQ,QAChB,IAAI;GACH,OAAA,GAAMA,sBAAAA,iBAAAA,CAAiB,OAAO;EAC/B,SAAS,OAAO;GACf,KAAK,WAAW,OAAO,KACtB;IAAE,KAAK;IAAO,UAAU,QAAQ;GAAS,GACzC,4CACD;GACA,OAAO,OAAO,KAAM,MAAgB,IAAI;EACzC;EAGD,QAAQ,QAAQ,MAAhB;GACC,KAAK;IACJ,OAAA,GAAMC,sBAAAA,uBAAAA,CACL,KAAK,QAAQ,oBAAoB,QAAQ,eACtC;KACA,GAAG;KACH,cAAc,gBACb,QAAQ,cACR,KAAK,QAAQ,gBACd;IACD,IACC,OACJ;IACA,KAAK,WAAW,OAAO,KACtB,EAAE,UAAU,QAAQ,SAAS,GAC7B,4BACD;IACA,OAAO;KAAE,QAAQ;KAAK,MAAM,EAAE,QAAQ,YAAY;IAAE;GACrD,KAAK,gBACJ,IAAI;IACH,MAAM,KAAK,OAAO,OAAO;IACzB,OAAO;KAAE,QAAQ;KAAK,MAAM,EAAE,QAAQ,KAAK;IAAE;GAC9C,SAAS,OAAO;IAGf,OAAO,OAAO,KAAM,MAAgB,IAAI;GACzC;GACD,SACC,OAAO;IAAE,QAAQ;IAAK,MAAM,EAAE,QAAQ,UAAU;GAAE;EACpD;CACD;CAEA,MAAc,OAAO,SAAwC;EAC5D,MAAM,YAAY,QAAQ;EAC1B,MAAM,SAAS,KAAK,WAAW,OAAO,MAAM,EAAE,UAAU,CAAC;EAEzD,OAAA,GAAMC,oBAAAA,sBAAAA,CACL;GACC;GACA;GACA,WAAW,KAAK,IAAI;GACpB,WAAW,cAAc,KAAK,WAAW,aAAa;EACvD,GACA,YAAY;GACX,IAAI;IACH,MAAM,EAAE,UAAU,OAAO,MAAMC,sBAAAA,kBAC9B,KAAK,YACL,KAAK,SACN;IACA,MAAM,SAASC,sBAAAA,iBACd,KAAK,aAAA,GACLC,sBAAAA,WAAAA,CAAW,OAAO,GAClB,MACD;IACA,MAAMC,sBAAAA,cAAc,KAAK,YAAY;KACpC;KACA;KACA;KACA;IACD,CAAC;GACF,SAAS,OAAO;IACf,OAAO,MAAM,OAAiB,6BAA6B;IAC3D,OAAA,GAAMC,kBAAAA,UAAAA,CAAU,KAAK;GACtB;EACD,CACD;CACD;AACD;;AAGA,SAAS,gBAAgB,KAAa,UAA0B;CAC/D,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,MAAM,SAAS,IAAI,IAAI,QAAQ;CAC/B,OAAO,WAAW,OAAO;CACzB,OAAO,OAAO,OAAO;CACrB,OAAO,OAAO,SAAS;AACxB;AAEA,SAAS,OACR,QACA,OACkB;CAClB,OAAO;EAAE;EAAQ,MAAM;GAAE,QAAQ;GAAW;EAAM;CAAE;AACrD"}
@@ -1,5 +1,5 @@
1
- import { n as Subscriber, t as SubscriberBuilder } from "../SubscriberBuilder-DMFk_Ap1.mjs";
2
- import { n as subscribedEvents, r as subscriberContext, t as runSubscriber } from "../runSubscriber-BYEY_bfA.mjs";
1
+ import { n as Subscriber, t as SubscriberBuilder } from "../SubscriberBuilder-CDy4nHCz.mjs";
2
+ import { n as subscribedEvents, r as subscriberContext, t as runSubscriber } from "../runSubscriber-BHvJ8POr.mjs";
3
3
  import { wrapError } from "@geekmidas/errors";
4
4
  import { runWithRequestContext } from "@geekmidas/services";
5
5
  import { confirmSnsSubscription, toSnsEvent, verifySnsMessage } from "@geekmidas/events/sns";
@@ -30,7 +30,7 @@ var SnsPushSubscriberAdaptor = class {
30
30
  await verifySnsMessage(message);
31
31
  } catch (error) {
32
32
  this.subscriber.logger.warn({
33
- error,
33
+ err: error,
34
34
  topicArn: message.TopicArn
35
35
  }, "Refused an SNS message that did not verify");
36
36
  return refuse(403, error.name);