@geekmidas/constructs 10.0.0-alpha.1 → 10.0.0-alpha.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -158
- package/dist/{Construct-CdhThxDI.mjs → Construct-B1vGBPme.mjs} +45 -33
- package/dist/Construct-B1vGBPme.mjs.map +1 -0
- package/dist/Construct-BQALFCh2.d.cts +230 -0
- package/dist/Construct-BQALFCh2.d.cts.map +1 -0
- package/dist/{Construct-roId57cU.cjs → Construct-DUSXuTHP.cjs} +65 -47
- package/dist/Construct-DUSXuTHP.cjs.map +1 -0
- package/dist/Construct-QXnl3VqT.d.mts +230 -0
- package/dist/Construct-QXnl3VqT.d.mts.map +1 -0
- package/dist/Cron-DI3nKoic.d.cts +45 -0
- package/dist/Cron-DI3nKoic.d.cts.map +1 -0
- package/dist/Cron-uYAHPnp7.d.mts +45 -0
- package/dist/Cron-uYAHPnp7.d.mts.map +1 -0
- package/dist/CronBuilder-C0Ek1-RA.cjs +121 -0
- package/dist/CronBuilder-C0Ek1-RA.cjs.map +1 -0
- package/dist/CronBuilder-DaSUm2Na.d.mts +47 -0
- package/dist/CronBuilder-DaSUm2Na.d.mts.map +1 -0
- package/dist/CronBuilder-Dmix_Pen.mjs +109 -0
- package/dist/CronBuilder-Dmix_Pen.mjs.map +1 -0
- package/dist/CronBuilder-g1lZLWyG.d.cts +47 -0
- package/dist/CronBuilder-g1lZLWyG.d.cts.map +1 -0
- package/dist/{Endpoint-BTj58Gob.mjs → Endpoint-BG4UJ9oA.mjs} +146 -109
- package/dist/Endpoint-BG4UJ9oA.mjs.map +1 -0
- package/dist/{Endpoint-BBkV6l0k.cjs → Endpoint-BoUg8ocM.cjs} +174 -131
- package/dist/Endpoint-BoUg8ocM.cjs.map +1 -0
- package/dist/{Endpoint-BKR5QH6O.d.mts → Endpoint-Du-NwPX2.d.mts} +83 -91
- package/dist/Endpoint-Du-NwPX2.d.mts.map +1 -0
- package/dist/{Endpoint-BQ9O0P1w.d.cts → Endpoint-aCvBMPoH.d.cts} +82 -90
- package/dist/Endpoint-aCvBMPoH.d.cts.map +1 -0
- package/dist/{EndpointFactory-BejSTzbB.mjs → EndpointFactory-BDtsrEvJ.mjs} +60 -77
- package/dist/EndpointFactory-BDtsrEvJ.mjs.map +1 -0
- package/dist/{EndpointFactory-DYoGLaIT.cjs → EndpointFactory-DJ7gXLpR.cjs} +96 -113
- package/dist/EndpointFactory-DJ7gXLpR.cjs.map +1 -0
- package/dist/{index-D5zqAJ2e.d.cts → EndpointFactory-DMYdJCOj.d.mts} +78 -134
- package/dist/EndpointFactory-DMYdJCOj.d.mts.map +1 -0
- package/dist/{index-Cvz551AO.d.mts → EndpointFactory-I7s1co3Y.d.cts} +79 -135
- package/dist/EndpointFactory-I7s1co3Y.d.cts.map +1 -0
- package/dist/Function-DuqGcbOI.d.mts +76 -0
- package/dist/Function-DuqGcbOI.d.mts.map +1 -0
- package/dist/Function-Pu-1kKZz.d.cts +76 -0
- package/dist/Function-Pu-1kKZz.d.cts.map +1 -0
- package/dist/{HonoEndpointAdaptor-7aYNdqJq.mjs → HonoEndpointAdaptor-PtPlbeKY.mjs} +75 -57
- package/dist/HonoEndpointAdaptor-PtPlbeKY.mjs.map +1 -0
- package/dist/{HonoEndpointAdaptor-wMsCyMgt.cjs → HonoEndpointAdaptor-zVzVLJEO.cjs} +83 -62
- package/dist/HonoEndpointAdaptor-zVzVLJEO.cjs.map +1 -0
- package/dist/Queue-A3wfD0it.cjs +109 -0
- package/dist/Queue-A3wfD0it.cjs.map +1 -0
- package/dist/Queue-D6USGS4q.mjs +104 -0
- package/dist/Queue-D6USGS4q.mjs.map +1 -0
- package/dist/{Queue-D6WeziRD.d.cts → Queue-DlsHRzUp.d.mts} +48 -48
- package/dist/Queue-DlsHRzUp.d.mts.map +1 -0
- package/dist/{Queue-DpUeqdC8.d.mts → Queue-HPyi0e1i.d.cts} +49 -49
- package/dist/Queue-HPyi0e1i.d.cts.map +1 -0
- package/dist/QueueBuilder-BjuF0UBl.cjs +135 -0
- package/dist/QueueBuilder-BjuF0UBl.cjs.map +1 -0
- package/dist/QueueBuilder-Cue73M4t.mjs +118 -0
- package/dist/QueueBuilder-Cue73M4t.mjs.map +1 -0
- package/dist/QueueBuilder-OSi6LkV9.d.cts +75 -0
- package/dist/QueueBuilder-OSi6LkV9.d.cts.map +1 -0
- package/dist/QueueBuilder-QHsnVt1P.d.mts +75 -0
- package/dist/QueueBuilder-QHsnVt1P.d.mts.map +1 -0
- package/dist/{Subscriber-4rBJgDKI.d.mts → Subscriber-Bpp02cGr.d.cts} +30 -21
- package/dist/Subscriber-Bpp02cGr.d.cts.map +1 -0
- package/dist/{Subscriber-DVQowdyj.d.cts → Subscriber-CtN9R48-.d.mts} +29 -20
- package/dist/Subscriber-CtN9R48-.d.mts.map +1 -0
- package/dist/SubscriberBuilder-CDy4nHCz.mjs +134 -0
- package/dist/SubscriberBuilder-CDy4nHCz.mjs.map +1 -0
- package/dist/SubscriberBuilder-CuCMFpYW.d.mts +67 -0
- package/dist/SubscriberBuilder-CuCMFpYW.d.mts.map +1 -0
- package/dist/SubscriberBuilder-D_owVoHo.cjs +145 -0
- package/dist/SubscriberBuilder-D_owVoHo.cjs.map +1 -0
- package/dist/SubscriberBuilder-g3fAVWgs.d.cts +67 -0
- package/dist/SubscriberBuilder-g3fAVWgs.d.cts.map +1 -0
- package/dist/Topic-D74b5cgs.mjs +99 -0
- package/dist/Topic-D74b5cgs.mjs.map +1 -0
- package/dist/Topic-GHxcU8AG.cjs +104 -0
- package/dist/Topic-GHxcU8AG.cjs.map +1 -0
- package/dist/adaptors/aws.cjs +332 -188
- package/dist/adaptors/aws.cjs.map +1 -1
- package/dist/adaptors/aws.d.cts +114 -44
- package/dist/adaptors/aws.d.cts.map +1 -1
- package/dist/adaptors/aws.d.mts +114 -44
- package/dist/adaptors/aws.d.mts.map +1 -1
- package/dist/adaptors/aws.mjs +308 -166
- package/dist/adaptors/aws.mjs.map +1 -1
- package/dist/adaptors/hono.cjs +3 -11
- package/dist/adaptors/hono.d.cts +7 -16
- package/dist/adaptors/hono.d.cts.map +1 -1
- package/dist/adaptors/hono.d.mts +8 -17
- package/dist/adaptors/hono.d.mts.map +1 -1
- package/dist/adaptors/hono.mjs +2 -11
- package/dist/adaptors/testing.cjs +743 -90
- package/dist/adaptors/testing.cjs.map +1 -1
- package/dist/adaptors/testing.d.cts +351 -54
- package/dist/adaptors/testing.d.cts.map +1 -1
- package/dist/adaptors/testing.d.mts +351 -54
- package/dist/adaptors/testing.d.mts.map +1 -1
- package/dist/adaptors/testing.mjs +706 -71
- package/dist/adaptors/testing.mjs.map +1 -1
- package/dist/auth-BMCJN30Y.d.cts +268 -0
- package/dist/auth-BMCJN30Y.d.cts.map +1 -0
- package/dist/auth-CcuceLYo.d.mts +268 -0
- package/dist/auth-CcuceLYo.d.mts.map +1 -0
- package/dist/auth.cjs +302 -34
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +2 -108
- package/dist/auth.d.mts +2 -108
- package/dist/auth.mjs +290 -28
- package/dist/auth.mjs.map +1 -1
- package/dist/{cache-CdQ7OLpR.d.cts → cache-CXh6GIgb.d.mts} +4 -7
- package/dist/cache-CXh6GIgb.d.mts.map +1 -0
- package/dist/{cache-DYoJFcfd.d.mts → cache-DnGDGly3.d.cts} +4 -7
- package/dist/cache-DnGDGly3.d.cts.map +1 -0
- package/dist/cache.cjs +68 -2
- package/dist/cache.cjs.map +1 -0
- package/dist/cache.d.cts +1 -2
- package/dist/cache.d.mts +1 -2
- package/dist/cache.mjs +67 -2
- package/dist/cache.mjs.map +1 -0
- package/dist/cipher-C1lEFX63.mjs +225 -0
- package/dist/cipher-C1lEFX63.mjs.map +1 -0
- package/dist/cipher-CVxfwLjk.cjs +332 -0
- package/dist/cipher-CVxfwLjk.cjs.map +1 -0
- package/dist/{clone-DblgEZZa.mjs → clone-EKNovYHu.mjs} +3 -3
- package/dist/clone-EKNovYHu.mjs.map +1 -0
- package/dist/{clone-Dy0cR2SZ.cjs → clone-UGJpvQnL.cjs} +7 -8
- package/dist/clone-UGJpvQnL.cjs.map +1 -0
- package/dist/{construct-interface-BqwRNYA2.d.mts → construct-interface-B1AfqRl4.d.cts} +43 -12
- package/dist/construct-interface-B1AfqRl4.d.cts.map +1 -0
- package/dist/{construct-interface-D2nNkHUN.d.cts → construct-interface-DluK3_da.d.mts} +43 -12
- package/dist/construct-interface-DluK3_da.d.mts.map +1 -0
- package/dist/construct-interface.cjs +113 -7
- package/dist/construct-interface.cjs.map +1 -0
- package/dist/construct-interface.d.cts +2 -2
- package/dist/construct-interface.d.mts +2 -2
- package/dist/construct-interface.mjs +106 -2
- package/dist/construct-interface.mjs.map +1 -0
- package/dist/credential.cjs +77 -17
- package/dist/credential.cjs.map +1 -1
- package/dist/credential.d.cts +34 -9
- package/dist/credential.d.cts.map +1 -1
- package/dist/credential.d.mts +34 -9
- package/dist/credential.d.mts.map +1 -1
- package/dist/credential.mjs +71 -13
- package/dist/credential.mjs.map +1 -1
- package/dist/crons/index.cjs +189 -85
- package/dist/crons/index.cjs.map +1 -1
- package/dist/crons/index.d.cts +70 -48
- package/dist/crons/index.d.cts.map +1 -1
- package/dist/crons/index.d.mts +70 -48
- package/dist/crons/index.d.mts.map +1 -1
- package/dist/crons/index.mjs +183 -81
- package/dist/crons/index.mjs.map +1 -1
- package/dist/database/kysely.cjs +4 -181
- package/dist/database/kysely.d.cts +2 -152
- package/dist/database/kysely.d.mts +2 -152
- package/dist/database/kysely.mjs +2 -180
- package/dist/email.cjs +25 -12
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +3 -7
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +3 -7
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +16 -3
- package/dist/email.mjs.map +1 -1
- package/dist/encryption.cjs +96 -0
- package/dist/encryption.cjs.map +1 -0
- package/dist/encryption.d.cts +117 -0
- package/dist/encryption.d.cts.map +1 -0
- package/dist/encryption.d.mts +117 -0
- package/dist/encryption.d.mts.map +1 -0
- package/dist/encryption.mjs +83 -0
- package/dist/encryption.mjs.map +1 -0
- package/dist/endpoints/index.cjs +8 -11
- package/dist/endpoints/index.d.cts +51 -7
- package/dist/endpoints/index.d.cts.map +1 -0
- package/dist/endpoints/index.d.mts +51 -7
- package/dist/endpoints/index.d.mts.map +1 -0
- package/dist/endpoints/index.mjs +6 -11
- package/dist/external-api-MJ8id87V.d.mts +105 -0
- package/dist/external-api-MJ8id87V.d.mts.map +1 -0
- package/dist/external-api-O107yzZJ.d.cts +105 -0
- package/dist/external-api-O107yzZJ.d.cts.map +1 -0
- package/dist/external-api.cjs +142 -0
- package/dist/external-api.cjs.map +1 -0
- package/dist/external-api.d.cts +2 -0
- package/dist/external-api.d.mts +2 -0
- package/dist/external-api.mjs +139 -0
- package/dist/external-api.mjs.map +1 -0
- package/dist/file-server.cjs +42 -10
- package/dist/file-server.cjs.map +1 -1
- package/dist/file-server.d.cts +6 -10
- package/dist/file-server.d.cts.map +1 -1
- package/dist/file-server.d.mts +6 -10
- package/dist/file-server.d.mts.map +1 -1
- package/dist/file-server.mjs +34 -2
- package/dist/file-server.mjs.map +1 -1
- package/dist/functions/index.cjs +2 -6
- package/dist/functions/index.d.cts +3 -4
- package/dist/functions/index.d.mts +3 -4
- package/dist/functions/index.mjs +2 -6
- package/dist/{functions-Cpff1uZH.mjs → functions-BFizikJU.mjs} +394 -243
- package/dist/functions-BFizikJU.mjs.map +1 -0
- package/dist/{functions-DukjG8ov.cjs → functions-CfzJG9hK.cjs} +432 -275
- package/dist/functions-CfzJG9hK.cjs.map +1 -0
- package/dist/index-BuzRvKOQ.d.cts +115 -0
- package/dist/index-BuzRvKOQ.d.cts.map +1 -0
- package/dist/index-CTkKOer4.d.mts +115 -0
- package/dist/index-CTkKOer4.d.mts.map +1 -0
- package/dist/index-CeEtxDmK.d.cts +79 -0
- package/dist/index-CeEtxDmK.d.cts.map +1 -0
- package/dist/index-CeEtxDmK.d.mts +79 -0
- package/dist/index-CeEtxDmK.d.mts.map +1 -0
- package/dist/index.cjs +6 -3
- package/dist/index.d.cts +27 -4
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +27 -4
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +3 -3
- package/dist/kms-B-Hm3d5r.cjs +85 -0
- package/dist/kms-B-Hm3d5r.cjs.map +1 -0
- package/dist/kms-BdUlMJEZ.mjs +85 -0
- package/dist/kms-BdUlMJEZ.mjs.map +1 -0
- package/dist/kysely-Bii4l6eP.mjs +370 -0
- package/dist/kysely-Bii4l6eP.mjs.map +1 -0
- package/dist/kysely-CO_Dk43W.cjs +383 -0
- package/dist/kysely-CO_Dk43W.cjs.map +1 -0
- package/dist/kysely-D0bpzgPs.d.cts +175 -0
- package/dist/kysely-D0bpzgPs.d.cts.map +1 -0
- package/dist/kysely-DUqC3Ie0.d.mts +175 -0
- package/dist/kysely-DUqC3Ie0.d.mts.map +1 -0
- package/dist/{telemetry-C3L0iS28.d.cts → lambdaTelemetry-8vnmVZQ5.d.cts} +3 -6
- package/dist/lambdaTelemetry-8vnmVZQ5.d.cts.map +1 -0
- package/dist/{telemetry-D7bVFLGi.d.mts → lambdaTelemetry-8vnmVZQ5.d.mts} +3 -6
- package/dist/lambdaTelemetry-8vnmVZQ5.d.mts.map +1 -0
- package/dist/{lazyAccessors-DM6HcXQk.mjs → lazyAccessors-CFanua0O.mjs} +20 -21
- package/dist/lazyAccessors-CFanua0O.mjs.map +1 -0
- package/dist/{lazyAccessors-DrJj8NpA.cjs → lazyAccessors-hJKGv8Ay.cjs} +60 -62
- package/dist/lazyAccessors-hJKGv8Ay.cjs.map +1 -0
- package/dist/mobile-app.cjs +73 -0
- package/dist/mobile-app.cjs.map +1 -0
- package/dist/mobile-app.d.cts +61 -0
- package/dist/mobile-app.d.cts.map +1 -0
- package/dist/mobile-app.d.mts +61 -0
- package/dist/mobile-app.d.mts.map +1 -0
- package/dist/mobile-app.mjs +72 -0
- package/dist/mobile-app.mjs.map +1 -0
- package/dist/object-storage.cjs +18 -10
- package/dist/object-storage.cjs.map +1 -1
- package/dist/object-storage.d.cts +3 -7
- package/dist/object-storage.d.cts.map +1 -1
- package/dist/object-storage.d.mts +3 -7
- package/dist/object-storage.d.mts.map +1 -1
- package/dist/object-storage.mjs +11 -3
- package/dist/object-storage.mjs.map +1 -1
- package/dist/processAudits-BRRVVHUB.mjs +131 -0
- package/dist/processAudits-BRRVVHUB.mjs.map +1 -0
- package/dist/processAudits-DasOZjPl.cjs +154 -0
- package/dist/processAudits-DasOZjPl.cjs.map +1 -0
- package/dist/queue/index.cjs +6 -189
- package/dist/queue/index.d.cts +3 -60
- package/dist/queue/index.d.mts +3 -60
- package/dist/queue/index.mjs +3 -186
- package/dist/rest-api.cjs +92 -63
- package/dist/rest-api.cjs.map +1 -1
- package/dist/rest-api.d.cts +81 -61
- package/dist/rest-api.d.cts.map +1 -1
- package/dist/rest-api.d.mts +81 -61
- package/dist/rest-api.d.mts.map +1 -1
- package/dist/rest-api.mjs +84 -55
- package/dist/rest-api.mjs.map +1 -1
- package/dist/{chunk-DWy1uDak.cjs → rolldown-runtime-y5ARvAq5.cjs} +14 -18
- package/dist/runSubscriber-BHvJ8POr.mjs +109 -0
- package/dist/runSubscriber-BHvJ8POr.mjs.map +1 -0
- package/dist/runSubscriber-D8nLXIv1.cjs +126 -0
- package/dist/runSubscriber-D8nLXIv1.cjs.map +1 -0
- package/dist/shutdown-BMJmAhSt.cjs +38 -0
- package/dist/shutdown-BMJmAhSt.cjs.map +1 -0
- package/dist/shutdown-C7xa8x9S.mjs +27 -0
- package/dist/shutdown-C7xa8x9S.mjs.map +1 -0
- package/dist/site.cjs +35 -9
- package/dist/site.cjs.map +1 -1
- package/dist/site.d.cts +25 -22
- package/dist/site.d.cts.map +1 -1
- package/dist/site.d.mts +25 -22
- package/dist/site.d.mts.map +1 -1
- package/dist/site.mjs +31 -5
- package/dist/site.mjs.map +1 -1
- package/dist/stages-BSCF19o2.d.cts +56 -0
- package/dist/stages-BSCF19o2.d.cts.map +1 -0
- package/dist/stages-BSCF19o2.d.mts +56 -0
- package/dist/stages-BSCF19o2.d.mts.map +1 -0
- package/dist/subscribers/index.cjs +111 -101
- package/dist/subscribers/index.cjs.map +1 -1
- package/dist/subscribers/index.d.cts +44 -56
- package/dist/subscribers/index.d.cts.map +1 -1
- package/dist/subscribers/index.d.mts +44 -56
- package/dist/subscribers/index.d.mts.map +1 -1
- package/dist/subscribers/index.mjs +108 -98
- package/dist/subscribers/index.mjs.map +1 -1
- package/dist/surfaceEnv-CNimK76p.cjs +56 -0
- package/dist/surfaceEnv-CNimK76p.cjs.map +1 -0
- package/dist/surfaceEnv-wMT5QzgZ.mjs +45 -0
- package/dist/surfaceEnv-wMT5QzgZ.mjs.map +1 -0
- package/dist/telemetry-B48RLjyk.d.cts +40 -0
- package/dist/telemetry-B48RLjyk.d.cts.map +1 -0
- package/dist/telemetry-DRtT1_0A.d.mts +40 -0
- package/dist/telemetry-DRtT1_0A.d.mts.map +1 -0
- package/dist/telemetry.cjs +56 -0
- package/dist/telemetry.cjs.map +1 -0
- package/dist/telemetry.d.cts +2 -0
- package/dist/telemetry.d.mts +2 -0
- package/dist/telemetry.mjs +55 -0
- package/dist/telemetry.mjs.map +1 -0
- package/dist/topic/index.cjs +3 -137
- package/dist/topic/index.d.cts +2 -32
- package/dist/topic/index.d.mts +2 -32
- package/dist/topic/index.mjs +2 -134
- package/dist/tracing-CxaZ98IB.mjs +110 -0
- package/dist/tracing-CxaZ98IB.mjs.map +1 -0
- package/dist/tracing-DPMidF_r.cjs +127 -0
- package/dist/tracing-DPMidF_r.cjs.map +1 -0
- package/dist/{types-BlAkSuGI.d.cts → types-DMuSQeLL.d.cts} +3 -5
- package/dist/types-DMuSQeLL.d.cts.map +1 -0
- package/dist/{types-aKksMEWL.d.mts → types-DMuSQeLL.d.mts} +3 -5
- package/dist/types-DMuSQeLL.d.mts.map +1 -0
- package/dist/types.d.cts +1 -1
- package/dist/types.d.mts +1 -1
- package/dist/types.mjs +1 -0
- package/dist/worker.cjs +191 -0
- package/dist/worker.cjs.map +1 -0
- package/dist/worker.d.cts +171 -0
- package/dist/worker.d.cts.map +1 -0
- package/dist/worker.d.mts +171 -0
- package/dist/worker.d.mts.map +1 -0
- package/dist/worker.mjs +190 -0
- package/dist/worker.mjs.map +1 -0
- package/package.json +116 -70
- package/dist/AWSScheduledFunction-9bJS4WdJ.d.cts +0 -122
- package/dist/AWSScheduledFunction-9bJS4WdJ.d.cts.map +0 -1
- package/dist/AWSScheduledFunction-BJjj8B5V.mjs +0 -243
- package/dist/AWSScheduledFunction-BJjj8B5V.mjs.map +0 -1
- package/dist/AWSScheduledFunction-BO5iEMSh.d.mts +0 -122
- package/dist/AWSScheduledFunction-BO5iEMSh.d.mts.map +0 -1
- package/dist/AWSScheduledFunction-qu4ieRZR.cjs +0 -255
- package/dist/AWSScheduledFunction-qu4ieRZR.cjs.map +0 -1
- package/dist/Construct-B-xc0SE9.d.mts +0 -104
- package/dist/Construct-B-xc0SE9.d.mts.map +0 -1
- package/dist/Construct-CdhThxDI.mjs.map +0 -1
- package/dist/Construct-WhCXIOPV.d.cts +0 -104
- package/dist/Construct-WhCXIOPV.d.cts.map +0 -1
- package/dist/Construct-roId57cU.cjs.map +0 -1
- package/dist/Endpoint-BBkV6l0k.cjs.map +0 -1
- package/dist/Endpoint-BKR5QH6O.d.mts.map +0 -1
- package/dist/Endpoint-BQ9O0P1w.d.cts.map +0 -1
- package/dist/Endpoint-BTj58Gob.mjs.map +0 -1
- package/dist/EndpointFactory-BejSTzbB.mjs.map +0 -1
- package/dist/EndpointFactory-DYoGLaIT.cjs.map +0 -1
- package/dist/HonoEndpointAdaptor-7aYNdqJq.mjs.map +0 -1
- package/dist/HonoEndpointAdaptor-wMsCyMgt.cjs.map +0 -1
- package/dist/Queue-D6WeziRD.d.cts.map +0 -1
- package/dist/Queue-DpUeqdC8.d.mts.map +0 -1
- package/dist/Subscriber-4rBJgDKI.d.mts.map +0 -1
- package/dist/Subscriber-DVQowdyj.d.cts.map +0 -1
- package/dist/Topic-Ohx9dvWr.d.cts +0 -91
- package/dist/Topic-Ohx9dvWr.d.cts.map +0 -1
- package/dist/Topic-cmy2jOUy.d.mts +0 -91
- package/dist/Topic-cmy2jOUy.d.mts.map +0 -1
- package/dist/auth.d.cts.map +0 -1
- package/dist/auth.d.mts.map +0 -1
- package/dist/cache-0ANvRQnD.mjs +0 -51
- package/dist/cache-0ANvRQnD.mjs.map +0 -1
- package/dist/cache-3WSvAulx.cjs +0 -57
- package/dist/cache-3WSvAulx.cjs.map +0 -1
- package/dist/cache-CdQ7OLpR.d.cts.map +0 -1
- package/dist/cache-DYoJFcfd.d.mts.map +0 -1
- package/dist/clone-DblgEZZa.mjs.map +0 -1
- package/dist/clone-Dy0cR2SZ.cjs.map +0 -1
- package/dist/construct-interface-BfjcOD_E.cjs +0 -117
- package/dist/construct-interface-BfjcOD_E.cjs.map +0 -1
- package/dist/construct-interface-BqwRNYA2.d.mts.map +0 -1
- package/dist/construct-interface-D2nNkHUN.d.cts.map +0 -1
- package/dist/construct-interface-Z4CpS8tK.mjs +0 -81
- package/dist/construct-interface-Z4CpS8tK.mjs.map +0 -1
- package/dist/database/kysely.cjs.map +0 -1
- package/dist/database/kysely.d.cts.map +0 -1
- package/dist/database/kysely.d.mts.map +0 -1
- package/dist/database/kysely.mjs.map +0 -1
- package/dist/functions-Cpff1uZH.mjs.map +0 -1
- package/dist/functions-DukjG8ov.cjs.map +0 -1
- package/dist/index-BTvozlUD.d.cts +0 -163
- package/dist/index-BTvozlUD.d.cts.map +0 -1
- package/dist/index-Cvz551AO.d.mts.map +0 -1
- package/dist/index-D5zqAJ2e.d.cts.map +0 -1
- package/dist/index-L7pz0R0T.d.mts +0 -162
- package/dist/index-L7pz0R0T.d.mts.map +0 -1
- package/dist/lazyAccessors-DM6HcXQk.mjs.map +0 -1
- package/dist/lazyAccessors-DrJj8NpA.cjs.map +0 -1
- package/dist/processAudits-Bc8rz6Ev.cjs +0 -92
- package/dist/processAudits-Bc8rz6Ev.cjs.map +0 -1
- package/dist/processAudits-D89tICM4.mjs +0 -80
- package/dist/processAudits-D89tICM4.mjs.map +0 -1
- package/dist/publisher-CGEo8UKD.mjs +0 -41
- package/dist/publisher-CGEo8UKD.mjs.map +0 -1
- package/dist/publisher-CqGsXB2X.cjs +0 -53
- package/dist/publisher-CqGsXB2X.cjs.map +0 -1
- package/dist/queue/index.cjs.map +0 -1
- package/dist/queue/index.d.cts.map +0 -1
- package/dist/queue/index.d.mts.map +0 -1
- package/dist/queue/index.mjs.map +0 -1
- package/dist/surfaceEnv-BFXEms2f.cjs +0 -39
- package/dist/surfaceEnv-BFXEms2f.cjs.map +0 -1
- package/dist/surfaceEnv-C3Ktahea.mjs +0 -27
- package/dist/surfaceEnv-C3Ktahea.mjs.map +0 -1
- package/dist/telemetry-C3L0iS28.d.cts.map +0 -1
- package/dist/telemetry-D7bVFLGi.d.mts.map +0 -1
- package/dist/topic/index.cjs.map +0 -1
- package/dist/topic/index.d.cts.map +0 -1
- package/dist/topic/index.d.mts.map +0 -1
- package/dist/topic/index.mjs.map +0 -1
- package/dist/types-BlAkSuGI.d.cts.map +0 -1
- package/dist/types-aKksMEWL.d.mts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-api.cjs","names":["canonicalId","provideKey","serviceKey","readCredentials","traceClient"],"sources":["../src/external-api.ts"],"sourcesContent":["/**\n * `ExternalApi` — an HTTP API somebody else runs.\n *\n * Polar, Stripe, a payment gateway: nothing to provision, but an address that\n * changes by stage and credentials the provider issued for each one. Without a\n * construct every integration writes that switching by hand — an optional\n * `POLAR_URL`, a fake started somewhere, a client built in a service — and the\n * target cannot see any of it.\n *\n * ```ts\n * // constructs/polar.ts\n * export const polar = new ExternalApi('Polar', {\n * url: 'https://www.polaraccesslink.com',\n * credentials: z.object({ clientId: z.string(), clientSecret: z.string() }),\n * client: ({ url, credentials }) => new PolarClient(url, credentials),\n * });\n *\n * // test/fakes/polar.ts\n * export default fake.app(new Hono().post('/v3/oauth2/token', …), {\n * credentials: { clientId: 'fake', clientSecret: 'fake' },\n * });\n *\n * // in a handler — the real API deployed, the fake locally and in tests\n * .dependsOn([polar])\n * .handle(({ services }) => services.polar.exchangeCode(code))\n * ```\n *\n * Deployed, the URL is `url` — one for every stage, or one per stage name with\n * a `default` — and the credentials are the stage's `<ID>_CREDENTIALS`, one\n * JSON value read the way a `Credential`'s is. Locally and in tests both come\n * from the fake.\n *\n * **The construct never names its fake.** It is found by convention, at\n * `test/fakes/<id>.ts`, and read only by gkm and the generated test harness —\n * so a fake, and the recorded responses it answers from, cannot end up in a\n * deployed bundle however the bundler follows imports.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { Consumable } from './construct-interface';\nimport { readCredentials } from './credential';\nimport type { DeployedStage, LocalStage } from './stages';\nimport { traceClient } from './tracing';\n\n/** What a credentials schema yields. */\ntype Value<TSchema extends StandardSchemaV1> =\n\tStandardSchemaV1.InferOutput<TSchema>;\n\n/**\n * Where an external API answers: one URL for every stage, or one per stage\n * name with `default` for any stage not listed.\n *\n * Keyed by the stages `gkm.config.ts` declares once `.gkm/stages.d.ts` exists,\n * so a key that names no stage — `prodution` — is a type error rather than a\n * URL nothing reads. Without the file any key goes, as before.\n */\nexport type ExternalApiUrl =\n\t| string\n\t| (string extends LocalStage | DeployedStage\n\t\t\t? Readonly<Record<string, string>>\n\t\t\t: { readonly [S in LocalStage | DeployedStage]?: string } & {\n\t\t\t\t\treadonly default?: string;\n\t\t\t\t});\n\nexport interface ExternalApiOptions<TSchema extends StandardSchemaV1, TClient> {\n\t/**\n\t * Where it answers deployed: one URL for every stage, or one per stage\n\t * name, with `default` for any stage not listed.\n\t */\n\turl: ExternalApiUrl;\n\t/** What the stage's `<ID>_CREDENTIALS` must look like. */\n\tcredentials: TSchema;\n\t/** What a handler is handed, built from where it answers and the credentials. */\n\tclient: (connection: {\n\t\turl: string;\n\t\tcredentials: Value<TSchema>;\n\t}) => TClient | Promise<TClient>;\n}\n\nexport class ExternalApi<\n\tTName extends string = string,\n\tTSchema extends StandardSchemaV1 = StandardSchemaV1,\n\tTClient = unknown,\n> implements Consumable<TName, TClient>\n{\n\t// `Consumable` for the reason `Credential` gives: a client generic in\n\t// `TClient` cannot satisfy `Construct`'s conditional service type.\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, TClient>;\n\n\t/** Read by both `declare()` and `connect()`, so the two cannot drift. */\n\tprivate readonly keys: { url: string; credentials: string };\n\n\t/** The promise, so concurrent registrations share one client. */\n\tprivate connected?: Promise<TClient>;\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly options: ExternalApiOptions<TSchema, TClient>,\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.keys = {\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\tcredentials: provideKey(canonical, 'credentials'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (registerOptions) => {\n\t\t\t\tthis.connected ??= this.connect(registerOptions);\n\t\t\t\treturn this.connected;\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * What the stage's `<ID>_CREDENTIALS` must look like — the schema the value\n\t * is validated against where it is read. Public so a tool setting the value\n\t * (`gkm secrets:add`) can check it, and describe its fields, before it is\n\t * stored rather than at the first request.\n\t */\n\tget credentialsSchema(): TSchema {\n\t\treturn this.options.credentials;\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'external-api',\n\t\t\t\tid: this.id,\n\t\t\t\t// The stage-keyed map is a record of URLs to the manifest, which\n\t\t\t\t// reads it by whatever stage it is resolving.\n\t\t\t\turl: this.options.url as Readonly<Record<string, string>> | string,\n\t\t\t\tprovides: [this.keys.url, this.keys.credentials],\n\t\t\t},\n\t\t];\n\t}\n\n\tprivate async connect(options: ServiceRegisterOptions): Promise<TClient> {\n\t\tconst { url } = options.envParser\n\t\t\t.create((get) => ({ url: get(this.keys.url).string() }))\n\t\t\t.parse();\n\t\tconst credentials = await readCredentials(\n\t\t\tthis.id,\n\t\t\tthis.keys.credentials,\n\t\t\tthis.options.credentials,\n\t\t\toptions.envParser,\n\t\t);\n\n\t\tconst client = await this.options.client({ url, credentials });\n\t\t// Each call a span carrying this API's name — the parent of the fetch\n\t\t// it makes, which is traced already.\n\t\treturn traceClient(this.id, client, {\n\t\t\t'server.address': hostOf(url),\n\t\t});\n\t}\n}\n\nfunction hostOf(url: string): string {\n\ttry {\n\t\treturn new URL(url).host;\n\t} catch {\n\t\treturn url;\n\t}\n}\n\n/** Anything that answers a request — a Hono app, or a bare `{ fetch }`. */\nexport interface FetchHandler {\n\tfetch(request: Request): Response | Promise<Response>;\n}\n\n/**\n * Marks a fake. A registered symbol, so a fake built by a second copy of this\n * package — a linked workspace, two versions in a lockfile — is still one.\n */\nconst FAKE = Symbol.for('@geekmidas/constructs/fake');\n\n/** What an external API's credentials look like going in, for a fake to supply. */\ntype CredentialsOf<TApi> =\n\tTApi extends ExternalApi<string, infer TSchema, unknown>\n\t\t? StandardSchemaV1.InferInput<TSchema>\n\t\t: unknown;\n\n/**\n * An external API's stand-in, as `test/fakes/<id>.ts` default-exports it.\n *\n * `credentials` are what the fake accepts, and what a local or test stage is\n * handed as `<ID>_CREDENTIALS` in place of the stage's own.\n */\nexport type Fake = {\n\treadonly [FAKE]: true;\n\treadonly credentials: unknown;\n} & (\n\t| {\n\t\t\treadonly kind: 'app';\n\t\t\t/** Served by gkm: in-process in a test, on its own port in `gkm dev`. */\n\t\t\treadonly handler: FetchHandler;\n\t }\n\t| {\n\t\t\treadonly kind: 'image';\n\t\t\t/** The provider's own local server, run as a container. */\n\t\t\treadonly image: string;\n\t\t\t/** The port it listens on inside the container. */\n\t\t\treadonly port: number;\n\t }\n);\n\n/**\n * Build the fake `test/fakes/<id>.ts` default-exports.\n *\n * Pass the API's type to have the credentials checked against its schema —\n * `fake.app<typeof polar>(…)`.\n */\nexport const fake = {\n\t/** A working implementation of the API, kept beside the tests that use it. */\n\tapp<TApi = unknown>(\n\t\thandler: FetchHandler,\n\t\toptions: { credentials: CredentialsOf<TApi> },\n\t): Fake {\n\t\treturn {\n\t\t\t[FAKE]: true,\n\t\t\tkind: 'app',\n\t\t\thandler,\n\t\t\tcredentials: options.credentials,\n\t\t};\n\t},\n\n\t/** A local server the provider publishes — `stripe/stripe-mock`. */\n\timage<TApi = unknown>(\n\t\timage: string,\n\t\toptions: { port: number; credentials: CredentialsOf<TApi> },\n\t): Fake {\n\t\treturn {\n\t\t\t[FAKE]: true,\n\t\t\tkind: 'image',\n\t\t\timage,\n\t\t\tport: options.port,\n\t\t\tcredentials: options.credentials,\n\t\t};\n\t},\n};\n\n/** Whether a value is a fake — what gkm asks of a `test/fakes` default export. */\nexport function isFake(value: unknown): value is Fake {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t(value as { [FAKE]?: unknown })[FAKE] === true\n\t);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFA,IAAa,cAAb,MAKA;CAcmB;CAXlB;CACA;;CAGA;;CAGA;CAEA,YACC,IACA,SACC;EADgB,KAAA,UAAA;EAEjB,MAAM,aAAA,GAAYA,oBAAAA,YAAAA,CAAY,EAAY;EAE1C,KAAK,KAAK;EACV,KAAK,OAAO;GACX,MAAA,GAAKC,oBAAAA,WAAAA,CAAW,WAAW,KAAK;GAChC,cAAA,GAAaA,oBAAAA,WAAAA,CAAW,WAAW,aAAa;EACjD;EAGA,KAAK,UAAU;GACd,cAAA,GAAaC,oBAAAA,WAAAA,CAAW,SAAS;GACjC,WAAW,oBAAoB;IAC9B,KAAK,cAAc,KAAK,QAAQ,eAAe;IAC/C,OAAO,KAAK;GACb;EACD;CACD;;;;;;;CAQA,IAAI,oBAA6B;EAChC,OAAO,KAAK,QAAQ;CACrB;CAEA,UAAyB;EACxB,OAAO,CACN;GACC,MAAM;GACN,IAAI,KAAK;GAGT,KAAK,KAAK,QAAQ;GAClB,UAAU,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,WAAW;EAChD,CACD;CACD;CAEA,MAAc,QAAQ,SAAmD;EACxE,MAAM,EAAE,QAAQ,QAAQ,UACtB,QAAQ,SAAS,EAAE,KAAK,IAAI,KAAK,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACvD,MAAM;EACR,MAAM,cAAc,MAAMC,mBAAAA,gBACzB,KAAK,IACL,KAAK,KAAK,aACV,KAAK,QAAQ,aACb,QAAQ,SACT;EAEA,MAAM,SAAS,MAAM,KAAK,QAAQ,OAAO;GAAE;GAAK;EAAY,CAAC;EAG7D,OAAOC,gBAAAA,YAAY,KAAK,IAAI,QAAQ,EACnC,kBAAkB,OAAO,GAAG,EAC7B,CAAC;CACF;AACD;AAEA,SAAS,OAAO,KAAqB;CACpC,IAAI;EACH,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACrB,QAAQ;EACP,OAAO;CACR;AACD;;;;;AAWA,MAAM,OAAO,OAAO,IAAI,4BAA4B;;;;;;;AAsCpD,MAAa,OAAO;;CAEnB,IACC,SACA,SACO;EACP,OAAO;IACL,OAAO;GACR,MAAM;GACN;GACA,aAAa,QAAQ;EACtB;CACD;;CAGA,MACC,OACA,SACO;EACP,OAAO;IACL,OAAO;GACR,MAAM;GACN;GACA,MAAM,QAAQ;GACd,aAAa,QAAQ;EACtB;CACD;AACD;;AAGA,SAAgB,OAAO,OAA+B;CACrD,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAA+B,UAAU;AAE5C"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readCredentials } from "./credential.mjs";
|
|
2
|
+
import { r as traceClient } from "./tracing-CxaZ98IB.mjs";
|
|
3
|
+
import { canonicalId, provideKey, serviceKey } from "@geekmidas/manifest";
|
|
4
|
+
//#region src/external-api.ts
|
|
5
|
+
/**
|
|
6
|
+
* `ExternalApi` — an HTTP API somebody else runs.
|
|
7
|
+
*
|
|
8
|
+
* Polar, Stripe, a payment gateway: nothing to provision, but an address that
|
|
9
|
+
* changes by stage and credentials the provider issued for each one. Without a
|
|
10
|
+
* construct every integration writes that switching by hand — an optional
|
|
11
|
+
* `POLAR_URL`, a fake started somewhere, a client built in a service — and the
|
|
12
|
+
* target cannot see any of it.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* // constructs/polar.ts
|
|
16
|
+
* export const polar = new ExternalApi('Polar', {
|
|
17
|
+
* url: 'https://www.polaraccesslink.com',
|
|
18
|
+
* credentials: z.object({ clientId: z.string(), clientSecret: z.string() }),
|
|
19
|
+
* client: ({ url, credentials }) => new PolarClient(url, credentials),
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // test/fakes/polar.ts
|
|
23
|
+
* export default fake.app(new Hono().post('/v3/oauth2/token', …), {
|
|
24
|
+
* credentials: { clientId: 'fake', clientSecret: 'fake' },
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // in a handler — the real API deployed, the fake locally and in tests
|
|
28
|
+
* .dependsOn([polar])
|
|
29
|
+
* .handle(({ services }) => services.polar.exchangeCode(code))
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Deployed, the URL is `url` — one for every stage, or one per stage name with
|
|
33
|
+
* a `default` — and the credentials are the stage's `<ID>_CREDENTIALS`, one
|
|
34
|
+
* JSON value read the way a `Credential`'s is. Locally and in tests both come
|
|
35
|
+
* from the fake.
|
|
36
|
+
*
|
|
37
|
+
* **The construct never names its fake.** It is found by convention, at
|
|
38
|
+
* `test/fakes/<id>.ts`, and read only by gkm and the generated test harness —
|
|
39
|
+
* so a fake, and the recorded responses it answers from, cannot end up in a
|
|
40
|
+
* deployed bundle however the bundler follows imports.
|
|
41
|
+
*/
|
|
42
|
+
var ExternalApi = class {
|
|
43
|
+
options;
|
|
44
|
+
id;
|
|
45
|
+
service;
|
|
46
|
+
/** Read by both `declare()` and `connect()`, so the two cannot drift. */
|
|
47
|
+
keys;
|
|
48
|
+
/** The promise, so concurrent registrations share one client. */
|
|
49
|
+
connected;
|
|
50
|
+
constructor(id, options) {
|
|
51
|
+
this.options = options;
|
|
52
|
+
const canonical = canonicalId(id);
|
|
53
|
+
this.id = canonical;
|
|
54
|
+
this.keys = {
|
|
55
|
+
url: provideKey(canonical, "url"),
|
|
56
|
+
credentials: provideKey(canonical, "credentials")
|
|
57
|
+
};
|
|
58
|
+
this.service = {
|
|
59
|
+
serviceName: serviceKey(canonical),
|
|
60
|
+
register: (registerOptions) => {
|
|
61
|
+
this.connected ??= this.connect(registerOptions);
|
|
62
|
+
return this.connected;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* What the stage's `<ID>_CREDENTIALS` must look like — the schema the value
|
|
68
|
+
* is validated against where it is read. Public so a tool setting the value
|
|
69
|
+
* (`gkm secrets:add`) can check it, and describe its fields, before it is
|
|
70
|
+
* stored rather than at the first request.
|
|
71
|
+
*/
|
|
72
|
+
get credentialsSchema() {
|
|
73
|
+
return this.options.credentials;
|
|
74
|
+
}
|
|
75
|
+
declare() {
|
|
76
|
+
return [{
|
|
77
|
+
kind: "external-api",
|
|
78
|
+
id: this.id,
|
|
79
|
+
url: this.options.url,
|
|
80
|
+
provides: [this.keys.url, this.keys.credentials]
|
|
81
|
+
}];
|
|
82
|
+
}
|
|
83
|
+
async connect(options) {
|
|
84
|
+
const { url } = options.envParser.create((get) => ({ url: get(this.keys.url).string() })).parse();
|
|
85
|
+
const credentials = await readCredentials(this.id, this.keys.credentials, this.options.credentials, options.envParser);
|
|
86
|
+
const client = await this.options.client({
|
|
87
|
+
url,
|
|
88
|
+
credentials
|
|
89
|
+
});
|
|
90
|
+
return traceClient(this.id, client, { "server.address": hostOf(url) });
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
function hostOf(url) {
|
|
94
|
+
try {
|
|
95
|
+
return new URL(url).host;
|
|
96
|
+
} catch {
|
|
97
|
+
return url;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Marks a fake. A registered symbol, so a fake built by a second copy of this
|
|
102
|
+
* package — a linked workspace, two versions in a lockfile — is still one.
|
|
103
|
+
*/
|
|
104
|
+
const FAKE = Symbol.for("@geekmidas/constructs/fake");
|
|
105
|
+
/**
|
|
106
|
+
* Build the fake `test/fakes/<id>.ts` default-exports.
|
|
107
|
+
*
|
|
108
|
+
* Pass the API's type to have the credentials checked against its schema —
|
|
109
|
+
* `fake.app<typeof polar>(…)`.
|
|
110
|
+
*/
|
|
111
|
+
const fake = {
|
|
112
|
+
/** A working implementation of the API, kept beside the tests that use it. */
|
|
113
|
+
app(handler, options) {
|
|
114
|
+
return {
|
|
115
|
+
[FAKE]: true,
|
|
116
|
+
kind: "app",
|
|
117
|
+
handler,
|
|
118
|
+
credentials: options.credentials
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
/** A local server the provider publishes — `stripe/stripe-mock`. */
|
|
122
|
+
image(image, options) {
|
|
123
|
+
return {
|
|
124
|
+
[FAKE]: true,
|
|
125
|
+
kind: "image",
|
|
126
|
+
image,
|
|
127
|
+
port: options.port,
|
|
128
|
+
credentials: options.credentials
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
/** Whether a value is a fake — what gkm asks of a `test/fakes` default export. */
|
|
133
|
+
function isFake(value) {
|
|
134
|
+
return typeof value === "object" && value !== null && value[FAKE] === true;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
export { ExternalApi, fake, isFake };
|
|
138
|
+
|
|
139
|
+
//# sourceMappingURL=external-api.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-api.mjs","names":[],"sources":["../src/external-api.ts"],"sourcesContent":["/**\n * `ExternalApi` — an HTTP API somebody else runs.\n *\n * Polar, Stripe, a payment gateway: nothing to provision, but an address that\n * changes by stage and credentials the provider issued for each one. Without a\n * construct every integration writes that switching by hand — an optional\n * `POLAR_URL`, a fake started somewhere, a client built in a service — and the\n * target cannot see any of it.\n *\n * ```ts\n * // constructs/polar.ts\n * export const polar = new ExternalApi('Polar', {\n * url: 'https://www.polaraccesslink.com',\n * credentials: z.object({ clientId: z.string(), clientSecret: z.string() }),\n * client: ({ url, credentials }) => new PolarClient(url, credentials),\n * });\n *\n * // test/fakes/polar.ts\n * export default fake.app(new Hono().post('/v3/oauth2/token', …), {\n * credentials: { clientId: 'fake', clientSecret: 'fake' },\n * });\n *\n * // in a handler — the real API deployed, the fake locally and in tests\n * .dependsOn([polar])\n * .handle(({ services }) => services.polar.exchangeCode(code))\n * ```\n *\n * Deployed, the URL is `url` — one for every stage, or one per stage name with\n * a `default` — and the credentials are the stage's `<ID>_CREDENTIALS`, one\n * JSON value read the way a `Credential`'s is. Locally and in tests both come\n * from the fake.\n *\n * **The construct never names its fake.** It is found by convention, at\n * `test/fakes/<id>.ts`, and read only by gkm and the generated test harness —\n * so a fake, and the recorded responses it answers from, cannot end up in a\n * deployed bundle however the bundler follows imports.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport type { Consumable } from './construct-interface';\nimport { readCredentials } from './credential';\nimport type { DeployedStage, LocalStage } from './stages';\nimport { traceClient } from './tracing';\n\n/** What a credentials schema yields. */\ntype Value<TSchema extends StandardSchemaV1> =\n\tStandardSchemaV1.InferOutput<TSchema>;\n\n/**\n * Where an external API answers: one URL for every stage, or one per stage\n * name with `default` for any stage not listed.\n *\n * Keyed by the stages `gkm.config.ts` declares once `.gkm/stages.d.ts` exists,\n * so a key that names no stage — `prodution` — is a type error rather than a\n * URL nothing reads. Without the file any key goes, as before.\n */\nexport type ExternalApiUrl =\n\t| string\n\t| (string extends LocalStage | DeployedStage\n\t\t\t? Readonly<Record<string, string>>\n\t\t\t: { readonly [S in LocalStage | DeployedStage]?: string } & {\n\t\t\t\t\treadonly default?: string;\n\t\t\t\t});\n\nexport interface ExternalApiOptions<TSchema extends StandardSchemaV1, TClient> {\n\t/**\n\t * Where it answers deployed: one URL for every stage, or one per stage\n\t * name, with `default` for any stage not listed.\n\t */\n\turl: ExternalApiUrl;\n\t/** What the stage's `<ID>_CREDENTIALS` must look like. */\n\tcredentials: TSchema;\n\t/** What a handler is handed, built from where it answers and the credentials. */\n\tclient: (connection: {\n\t\turl: string;\n\t\tcredentials: Value<TSchema>;\n\t}) => TClient | Promise<TClient>;\n}\n\nexport class ExternalApi<\n\tTName extends string = string,\n\tTSchema extends StandardSchemaV1 = StandardSchemaV1,\n\tTClient = unknown,\n> implements Consumable<TName, TClient>\n{\n\t// `Consumable` for the reason `Credential` gives: a client generic in\n\t// `TClient` cannot satisfy `Construct`'s conditional service type.\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, TClient>;\n\n\t/** Read by both `declare()` and `connect()`, so the two cannot drift. */\n\tprivate readonly keys: { url: string; credentials: string };\n\n\t/** The promise, so concurrent registrations share one client. */\n\tprivate connected?: Promise<TClient>;\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly options: ExternalApiOptions<TSchema, TClient>,\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.keys = {\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\tcredentials: provideKey(canonical, 'credentials'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (registerOptions) => {\n\t\t\t\tthis.connected ??= this.connect(registerOptions);\n\t\t\t\treturn this.connected;\n\t\t\t},\n\t\t};\n\t}\n\n\t/**\n\t * What the stage's `<ID>_CREDENTIALS` must look like — the schema the value\n\t * is validated against where it is read. Public so a tool setting the value\n\t * (`gkm secrets:add`) can check it, and describe its fields, before it is\n\t * stored rather than at the first request.\n\t */\n\tget credentialsSchema(): TSchema {\n\t\treturn this.options.credentials;\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'external-api',\n\t\t\t\tid: this.id,\n\t\t\t\t// The stage-keyed map is a record of URLs to the manifest, which\n\t\t\t\t// reads it by whatever stage it is resolving.\n\t\t\t\turl: this.options.url as Readonly<Record<string, string>> | string,\n\t\t\t\tprovides: [this.keys.url, this.keys.credentials],\n\t\t\t},\n\t\t];\n\t}\n\n\tprivate async connect(options: ServiceRegisterOptions): Promise<TClient> {\n\t\tconst { url } = options.envParser\n\t\t\t.create((get) => ({ url: get(this.keys.url).string() }))\n\t\t\t.parse();\n\t\tconst credentials = await readCredentials(\n\t\t\tthis.id,\n\t\t\tthis.keys.credentials,\n\t\t\tthis.options.credentials,\n\t\t\toptions.envParser,\n\t\t);\n\n\t\tconst client = await this.options.client({ url, credentials });\n\t\t// Each call a span carrying this API's name — the parent of the fetch\n\t\t// it makes, which is traced already.\n\t\treturn traceClient(this.id, client, {\n\t\t\t'server.address': hostOf(url),\n\t\t});\n\t}\n}\n\nfunction hostOf(url: string): string {\n\ttry {\n\t\treturn new URL(url).host;\n\t} catch {\n\t\treturn url;\n\t}\n}\n\n/** Anything that answers a request — a Hono app, or a bare `{ fetch }`. */\nexport interface FetchHandler {\n\tfetch(request: Request): Response | Promise<Response>;\n}\n\n/**\n * Marks a fake. A registered symbol, so a fake built by a second copy of this\n * package — a linked workspace, two versions in a lockfile — is still one.\n */\nconst FAKE = Symbol.for('@geekmidas/constructs/fake');\n\n/** What an external API's credentials look like going in, for a fake to supply. */\ntype CredentialsOf<TApi> =\n\tTApi extends ExternalApi<string, infer TSchema, unknown>\n\t\t? StandardSchemaV1.InferInput<TSchema>\n\t\t: unknown;\n\n/**\n * An external API's stand-in, as `test/fakes/<id>.ts` default-exports it.\n *\n * `credentials` are what the fake accepts, and what a local or test stage is\n * handed as `<ID>_CREDENTIALS` in place of the stage's own.\n */\nexport type Fake = {\n\treadonly [FAKE]: true;\n\treadonly credentials: unknown;\n} & (\n\t| {\n\t\t\treadonly kind: 'app';\n\t\t\t/** Served by gkm: in-process in a test, on its own port in `gkm dev`. */\n\t\t\treadonly handler: FetchHandler;\n\t }\n\t| {\n\t\t\treadonly kind: 'image';\n\t\t\t/** The provider's own local server, run as a container. */\n\t\t\treadonly image: string;\n\t\t\t/** The port it listens on inside the container. */\n\t\t\treadonly port: number;\n\t }\n);\n\n/**\n * Build the fake `test/fakes/<id>.ts` default-exports.\n *\n * Pass the API's type to have the credentials checked against its schema —\n * `fake.app<typeof polar>(…)`.\n */\nexport const fake = {\n\t/** A working implementation of the API, kept beside the tests that use it. */\n\tapp<TApi = unknown>(\n\t\thandler: FetchHandler,\n\t\toptions: { credentials: CredentialsOf<TApi> },\n\t): Fake {\n\t\treturn {\n\t\t\t[FAKE]: true,\n\t\t\tkind: 'app',\n\t\t\thandler,\n\t\t\tcredentials: options.credentials,\n\t\t};\n\t},\n\n\t/** A local server the provider publishes — `stripe/stripe-mock`. */\n\timage<TApi = unknown>(\n\t\timage: string,\n\t\toptions: { port: number; credentials: CredentialsOf<TApi> },\n\t): Fake {\n\t\treturn {\n\t\t\t[FAKE]: true,\n\t\t\tkind: 'image',\n\t\t\timage,\n\t\t\tport: options.port,\n\t\t\tcredentials: options.credentials,\n\t\t};\n\t},\n};\n\n/** Whether a value is a fake — what gkm asks of a `test/fakes` default export. */\nexport function isFake(value: unknown): value is Fake {\n\treturn (\n\t\ttypeof value === 'object' &&\n\t\tvalue !== null &&\n\t\t(value as { [FAKE]?: unknown })[FAKE] === true\n\t);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuFA,IAAa,cAAb,MAKA;CAcmB;CAXlB;CACA;;CAGA;;CAGA;CAEA,YACC,IACA,SACC;EADgB,KAAA,UAAA;EAEjB,MAAM,YAAY,YAAY,EAAY;EAE1C,KAAK,KAAK;EACV,KAAK,OAAO;GACX,KAAK,WAAW,WAAW,KAAK;GAChC,aAAa,WAAW,WAAW,aAAa;EACjD;EAGA,KAAK,UAAU;GACd,aAAa,WAAW,SAAS;GACjC,WAAW,oBAAoB;IAC9B,KAAK,cAAc,KAAK,QAAQ,eAAe;IAC/C,OAAO,KAAK;GACb;EACD;CACD;;;;;;;CAQA,IAAI,oBAA6B;EAChC,OAAO,KAAK,QAAQ;CACrB;CAEA,UAAyB;EACxB,OAAO,CACN;GACC,MAAM;GACN,IAAI,KAAK;GAGT,KAAK,KAAK,QAAQ;GAClB,UAAU,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,WAAW;EAChD,CACD;CACD;CAEA,MAAc,QAAQ,SAAmD;EACxE,MAAM,EAAE,QAAQ,QAAQ,UACtB,QAAQ,SAAS,EAAE,KAAK,IAAI,KAAK,KAAK,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACvD,MAAM;EACR,MAAM,cAAc,MAAM,gBACzB,KAAK,IACL,KAAK,KAAK,aACV,KAAK,QAAQ,aACb,QAAQ,SACT;EAEA,MAAM,SAAS,MAAM,KAAK,QAAQ,OAAO;GAAE;GAAK;EAAY,CAAC;EAG7D,OAAO,YAAY,KAAK,IAAI,QAAQ,EACnC,kBAAkB,OAAO,GAAG,EAC7B,CAAC;CACF;AACD;AAEA,SAAS,OAAO,KAAqB;CACpC,IAAI;EACH,OAAO,IAAI,IAAI,GAAG,CAAC,CAAC;CACrB,QAAQ;EACP,OAAO;CACR;AACD;;;;;AAWA,MAAM,OAAO,OAAO,IAAI,4BAA4B;;;;;;;AAsCpD,MAAa,OAAO;;CAEnB,IACC,SACA,SACO;EACP,OAAO;IACL,OAAO;GACR,MAAM;GACN;GACA,aAAa,QAAQ;EACtB;CACD;;CAGA,MACC,OACA,SACO;EACP,OAAO;IACL,OAAO;GACR,MAAM;GACN;GACA,MAAM,QAAQ;GACd,aAAa,QAAQ;EACtB;CACD;AACD;;AAGA,SAAgB,OAAO,OAA+B;CACrD,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAA+B,UAAU;AAE5C"}
|
package/dist/file-server.cjs
CHANGED
|
@@ -1,10 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _geekmidas_manifest = require("@geekmidas/manifest");
|
|
3
|
+
let _geekmidas_storage = require("@geekmidas/storage");
|
|
5
4
|
//#region src/file-server.ts
|
|
5
|
+
/**
|
|
6
|
+
* `FileServer` — a domain that serves a bucket's objects.
|
|
7
|
+
*
|
|
8
|
+
* `ObjectStorage` is the bucket: it writes, it presigns, and its URL is never
|
|
9
|
+
* public. Serving those objects on a domain is a different thing, and it is a
|
|
10
|
+
* construct of its own rather than a flag on the bucket — see
|
|
11
|
+
* `docs/design/constructs-paradigm.md`, "Why a separate construct, and what it
|
|
12
|
+
* costs", for the comparison that rules the other two shapes out.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* export const uploads = new FileServer('Uploads', { open: ['brand/**'] });
|
|
16
|
+
*
|
|
17
|
+
* uploads.getUploadURL({ path, contentType }) // an S3 presign at the bucket
|
|
18
|
+
* uploads.url('brand/logo.png') // open path — no signature
|
|
19
|
+
* uploads.signedUrl('invoices/7.pdf') // one object, one recipient
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* **One construct, one client, both halves.** How a file lands in the bucket is
|
|
23
|
+
* the same whether or not it is served, so splitting the upload half from the
|
|
24
|
+
* serving half would be ceremony rather than safety. The client is a *superset*
|
|
25
|
+
* of `StorageClient`, made literal in the type, so anything taking
|
|
26
|
+
* `services.uploads` keeps compiling when the construct behind it grows a
|
|
27
|
+
* serving half.
|
|
28
|
+
*
|
|
29
|
+
* **The id names the bucket, not the surface.** This is the detail that has to
|
|
30
|
+
* be right before anything ships, because getting it backwards is destructive:
|
|
31
|
+
* the migration people actually perform is serving a bucket they already have,
|
|
32
|
+
* and naming the bucket after the construct makes that edit *add* a node rather
|
|
33
|
+
* than rename the one holding the data.
|
|
34
|
+
*/
|
|
6
35
|
/** A key was asked to be served unsigned, and no open pattern admits it. */
|
|
7
36
|
var NotOpen = class extends Error {
|
|
37
|
+
key;
|
|
38
|
+
open;
|
|
8
39
|
constructor(key, open) {
|
|
9
40
|
super(`'${key}' is not served without a signature. ` + (open.length ? `Open paths: ${open.join(", ")}. Use signedUrl() for anything else.` : "This file server declares no open paths, so every object needs signedUrl()."));
|
|
10
41
|
this.key = key;
|
|
@@ -13,6 +44,7 @@ var NotOpen = class extends Error {
|
|
|
13
44
|
}
|
|
14
45
|
};
|
|
15
46
|
var FileServer = class {
|
|
47
|
+
options;
|
|
16
48
|
id;
|
|
17
49
|
service;
|
|
18
50
|
/** The id of the surface node, as opposed to the bucket's. */
|
|
@@ -25,17 +57,17 @@ var FileServer = class {
|
|
|
25
57
|
keys;
|
|
26
58
|
constructor(id, options = {}) {
|
|
27
59
|
this.options = options;
|
|
28
|
-
const canonical = (0,
|
|
60
|
+
const canonical = (0, _geekmidas_manifest.canonicalId)(id);
|
|
29
61
|
this.id = canonical;
|
|
30
62
|
this.open = options.open ?? [];
|
|
31
63
|
const originId = options.origin?.id ?? canonical;
|
|
32
64
|
this.surfaceId = `${canonical}Server`;
|
|
33
65
|
this.keys = {
|
|
34
|
-
origin: (0,
|
|
35
|
-
server: (0,
|
|
66
|
+
origin: (0, _geekmidas_manifest.provideKey)(originId, "url"),
|
|
67
|
+
server: (0, _geekmidas_manifest.provideKey)(this.surfaceId, "url")
|
|
36
68
|
};
|
|
37
69
|
this.service = {
|
|
38
|
-
serviceName: (0,
|
|
70
|
+
serviceName: (0, _geekmidas_manifest.serviceKey)(canonical),
|
|
39
71
|
register: (registerOptions) => this.connect(registerOptions)
|
|
40
72
|
};
|
|
41
73
|
}
|
|
@@ -99,7 +131,7 @@ var FileServer = class {
|
|
|
99
131
|
origin: get(this.keys.origin).string(),
|
|
100
132
|
server: get(this.keys.server).string()
|
|
101
133
|
})).parse();
|
|
102
|
-
const storage = (0,
|
|
134
|
+
const storage = (0, _geekmidas_storage.createStorageClient)(origin);
|
|
103
135
|
const base = server.replace(/\/+$/, "");
|
|
104
136
|
const serve = (key) => {
|
|
105
137
|
if (!this.serves(key)) throw new NotOpen(key, this.open);
|
|
@@ -147,8 +179,8 @@ function globToRegExp(pattern) {
|
|
|
147
179
|
}
|
|
148
180
|
return new RegExp(`^${source}$`);
|
|
149
181
|
}
|
|
150
|
-
|
|
151
182
|
//#endregion
|
|
152
183
|
exports.FileServer = FileServer;
|
|
153
184
|
exports.NotOpen = NotOpen;
|
|
185
|
+
|
|
154
186
|
//# sourceMappingURL=file-server.cjs.map
|
package/dist/file-server.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-server.cjs","names":["key: string","open: readonly string[]","id: ConstructName<TName>","options: FileServerOptions<TOpen>","surface: Declaration","options: ServiceRegisterOptions","expiresIn?: number","pattern: string"],"sources":["../src/file-server.ts"],"sourcesContent":["/**\n * `FileServer` — a domain that serves a bucket's objects.\n *\n * `ObjectStorage` is the bucket: it writes, it presigns, and its URL is never\n * public. Serving those objects on a domain is a different thing, and it is a\n * construct of its own rather than a flag on the bucket — see\n * `docs/design/constructs-paradigm.md`, \"Why a separate construct, and what it\n * costs\", for the comparison that rules the other two shapes out.\n *\n * ```ts\n * export const uploads = new FileServer('Uploads', { open: ['brand/**'] });\n *\n * uploads.getUploadURL({ path, contentType }) // an S3 presign at the bucket\n * uploads.url('brand/logo.png') // open path — no signature\n * uploads.signedUrl('invoices/7.pdf') // one object, one recipient\n * ```\n *\n * **One construct, one client, both halves.** How a file lands in the bucket is\n * the same whether or not it is served, so splitting the upload half from the\n * serving half would be ceremony rather than safety. The client is a *superset*\n * of `StorageClient`, made literal in the type, so anything taking\n * `services.uploads` keeps compiling when the construct behind it grows a\n * serving half.\n *\n * **The id names the bucket, not the surface.** This is the detail that has to\n * be right before anything ships, because getting it backwards is destructive:\n * the migration people actually perform is serving a bucket they already have,\n * and naming the bucket after the construct makes that edit *add* a node rather\n * than rename the one holding the data.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport { createStorageClient, type StorageClient } from '@geekmidas/storage';\nimport type { Construct, Declarable } from './construct-interface';\n\n/**\n * A key an open pattern admits.\n *\n * The same template-literal trick `ConstructName` uses, so `open` is checked at\n * the call site rather than only at the edge:\n *\n * ```ts\n * files.url('brand/logo.png') // ok\n * files.url(`avatars/${id}.png`) // ok — a template literal still matches\n * files.url('invoices/7.pdf') // not assignable — never served unsigned\n * ```\n *\n * A single star and a double star resolve to the same type, stated rather than\n * discovered: a template literal cannot exclude `/`, so this is a prefix-and-\n * suffix guard while the *exact* pattern is enforced by the bucket policy, by\n * the cache behaviour, and by this construct's own runtime check. The type says\n * plausibly open; the infrastructure says definitely.\n */\nexport type Served<P extends string> = P extends `${infer Head}**`\n\t? `${Head}${string}`\n\t: P extends `${infer Head}*${infer Tail}`\n\t\t? `${Head}${string}${Tail}`\n\t\t: P;\n\n/** What consuming a file server hands you: a bucket client that also serves. */\nexport interface FileServerClient<TOpen extends string = never>\n\textends StorageClient {\n\t/**\n\t * The unsigned address of an open object.\n\t *\n\t * Compile-checked against the declared patterns, *and* checked again at\n\t * runtime — a JavaScript caller gets no compiler, and minting an unsigned\n\t * URL for a private object is a leak rather than a mistake.\n\t *\n\t * @throws {NotOpen} when the key matches no declared pattern.\n\t */\n\turl(key: Served<TOpen>): string;\n\n\t/**\n\t * The same thing for a key the compiler cannot see.\n\t *\n\t * A fully dynamic key does not type, which is correct and is also the case\n\t * people will hit — so the escape hatch is a named method rather than a\n\t * cast, and it runs the *same* runtime check rather than being a way around\n\t * it.\n\t *\n\t * @throws {NotOpen} when the key matches no declared pattern.\n\t */\n\topenUrl(key: string): string;\n\n\t/**\n\t * A signed address for one object and one recipient.\n\t *\n\t * Today this is an **S3 presign at the bucket**, which is what works\n\t * unchanged against MinIO locally and against S3 deployed. It is deliberately\n\t * not described as a CDN URL: CloudFront signing is a different mechanism,\n\t * with different keys, against a different host, and it needs key material\n\t * this construct does not yet declare.\n\t *\n\t * @param expiresIn - minutes, matching `getUploadURL`.\n\t */\n\tsignedUrl(key: string, expiresIn?: number): Promise<string>;\n}\n\nexport interface FileServerOptions<TOpen extends string = never> {\n\t/**\n\t * Paths served without a signature. Everything else needs one.\n\t *\n\t * Private by default: a bucket where forgetting a flag publishes user\n\t * uploads is the wrong default, so this is an exception list, and listing it\n\t * in code means exactly one set of paths is unauthenticated and it is\n\t * reviewable in a diff.\n\t *\n\t * Uploading is deliberately *not* restricted to these. Writing to a path\n\t * that happens to be open is ordinary; the only asymmetry worth enforcing is\n\t * that you cannot hand out an unsigned URL for something the server would\n\t * refuse to serve unsigned.\n\t */\n\topen?: readonly TOpen[];\n\t/** Keep previous versions of an object. */\n\tversioned?: boolean;\n\t/**\n\t * A bucket declared elsewhere, instead of one of this server's own.\n\t *\n\t * `new FileServer('Uploads')` declares both nodes; this fronts a bucket that\n\t * already exists, which is also how the two edges come back when you want\n\t * them separately — a handler depending on the bucket writes, a handler\n\t * depending on the server mints URLs.\n\t */\n\torigin?: Declarable;\n}\n\n/** A key was asked to be served unsigned, and no open pattern admits it. */\nexport class NotOpen extends Error {\n\tconstructor(\n\t\treadonly key: string,\n\t\treadonly open: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`'${key}' is not served without a signature. ` +\n\t\t\t\t(open.length\n\t\t\t\t\t? `Open paths: ${open.join(', ')}. Use signedUrl() for anything else.`\n\t\t\t\t\t: 'This file server declares no open paths, so every object needs signedUrl().'),\n\t\t);\n\t\tthis.name = 'NotOpen';\n\t}\n}\n\nexport class FileServer<\n\tTName extends string = string,\n\tTOpen extends string = never,\n> implements Construct<TName, FileServerClient<TOpen>>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, FileServerClient<TOpen>>;\n\n\t/** The id of the surface node, as opposed to the bucket's. */\n\treadonly surfaceId: string;\n\n\tprivate readonly open: readonly string[];\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the keys the\n\t * target publishes and the keys the client reads cannot drift.\n\t */\n\tprivate readonly keys: { origin: string; server: string };\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly options: FileServerOptions<TOpen> = {},\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.open = options.open ?? [];\n\n\t\t// The surface takes the suffix and the bucket keeps the plain id, which\n\t\t// is what makes \"serve a bucket I already have\" an edit that adds a node\n\t\t// instead of renaming the one holding the data.\n\t\tconst originId = options.origin?.id ?? canonical;\n\t\tthis.surfaceId = `${canonical}Server`;\n\n\t\tthis.keys = {\n\t\t\torigin: provideKey(originId, 'url'),\n\t\t\tserver: provideKey(this.surfaceId, 'url'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (registerOptions) => this.connect(registerOptions),\n\t\t};\n\t}\n\n\t/**\n\t * The served surface, as something an edge can point at.\n\t *\n\t * Depending on the `FileServer` itself points at the **bucket** — its\n\t * canonical node — which is right for a handler: it presigns and writes. But\n\t * a bucket's URL is never public, so a site depending on it gets nothing\n\t * inlined, which is correct and also useless.\n\t *\n\t * This is the other half: `.dependsOn([uploads.server])` points at the\n\t * surface, whose address *is* public, and is what puts\n\t * `VITE_UPLOADS_SERVER_URL` in a bundle. The same accessor shape\n\t * `database.reader()` uses, and for the same reason — one construct owning\n\t * two addresses needs a way to name the second.\n\t */\n\tget server(): Declarable {\n\t\tconst surface = this.declare().find(\n\t\t\t(declaration) => declaration.id === this.surfaceId,\n\t\t);\n\n\t\treturn {\n\t\t\tid: this.surfaceId,\n\t\t\tdeclare: () => (surface ? [surface] : []),\n\t\t};\n\t}\n\n\t/**\n\t * The bucket and the surface over it — or just the surface, when the bucket\n\t * was declared elsewhere.\n\t *\n\t * The surface names its origin through `of`, which is what makes the\n\t * reference check work: an origin resolving to nothing is a build failure,\n\t * the same guarantee `assertDerivations` already gives a reader and a schema\n\t * tenant.\n\t */\n\tdeclare(): Declaration[] {\n\t\tconst origin = this.options.origin;\n\n\t\tconst surface: Declaration = {\n\t\t\tkind: 'file-server',\n\t\t\tid: this.surfaceId,\n\t\t\tof: origin?.id ?? this.id,\n\t\t\t...(this.open.length ? { open: this.open } : {}),\n\t\t\tprovides: [this.keys.server],\n\t\t};\n\n\t\t// A vended bucket is declared here; a given one already declares itself,\n\t\t// and declaring it twice is the duplication the whole model removes.\n\t\tif (origin) return [surface];\n\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'objects',\n\t\t\t\tid: this.id,\n\t\t\t\tprovides: [this.keys.origin],\n\t\t\t\t...(this.options.versioned ? { versioned: true } : {}),\n\t\t\t},\n\t\t\tsurface,\n\t\t];\n\t}\n\n\t/**\n\t * The bucket's client, with the serving half added.\n\t *\n\t * Both addresses are read here, which is also how the environment reaches a\n\t * handler: the sniffer records what this reads, so `.dependsOn([uploads])`\n\t * carries the bucket URL and the server URL without either being written\n\t * down beside the handler.\n\t */\n\tprivate async connect(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<FileServerClient<TOpen>> {\n\t\tconst { origin, server } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\torigin: get(this.keys.origin).string(),\n\t\t\t\tserver: get(this.keys.server).string(),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\tconst storage = createStorageClient(origin);\n\t\tconst base = server.replace(/\\/+$/, '');\n\n\t\tconst serve = (key: string): string => {\n\t\t\tif (!this.serves(key)) throw new NotOpen(key, this.open);\n\n\t\t\treturn `${base}/${key.replace(/^\\/+/, '')}`;\n\t\t};\n\n\t\t// Delegating rather than copying: the bucket half is whatever the driver\n\t\t// produced, including anything it grows later, and only the serving half\n\t\t// is added on top.\n\t\treturn Object.assign(Object.create(storage) as StorageClient, {\n\t\t\turl: serve as (key: Served<TOpen>) => string,\n\t\t\topenUrl: serve,\n\t\t\tsignedUrl: (key: string, expiresIn?: number) =>\n\t\t\t\tstorage.getDownloadURL({ path: key } as never, expiresIn),\n\t\t}) as FileServerClient<TOpen>;\n\t}\n\n\t/**\n\t * Whether an open pattern admits a key.\n\t *\n\t * The runtime half of {@link Served}, and stricter than it: here a single\n\t * star stops at a segment boundary and a double star does not, which is the\n\t * distinction a template-literal type cannot draw and the infrastructure\n\t * does.\n\t */\n\tprivate serves(key: string): boolean {\n\t\treturn this.open.some((pattern) => globToRegExp(pattern).test(key));\n\t}\n}\n\n/**\n * One open pattern as an anchored expression.\n *\n * A double star crosses `/`; a single star does not. Both are consumed in one\n * left-to-right pass rather than by substitution, so a pattern cannot be\n * rewritten into a placeholder that a later pass mistakes for literal text.\n */\nfunction globToRegExp(pattern: string): RegExp {\n\tlet source = '';\n\n\tfor (let index = 0; index < pattern.length; index++) {\n\t\tconst character = pattern[index];\n\n\t\tif (character !== '*') {\n\t\t\tsource += character?.replace(/[.+^${}()|[\\]\\\\?]/, '\\\\$&') ?? '';\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (pattern[index + 1] === '*') {\n\t\t\tsource += '.*';\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tsource += '[^/]*';\n\t}\n\n\treturn new RegExp(`^${source}$`);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-server.cjs","names":["canonicalId","provideKey","serviceKey","createStorageClient"],"sources":["../src/file-server.ts"],"sourcesContent":["/**\n * `FileServer` — a domain that serves a bucket's objects.\n *\n * `ObjectStorage` is the bucket: it writes, it presigns, and its URL is never\n * public. Serving those objects on a domain is a different thing, and it is a\n * construct of its own rather than a flag on the bucket — see\n * `docs/design/constructs-paradigm.md`, \"Why a separate construct, and what it\n * costs\", for the comparison that rules the other two shapes out.\n *\n * ```ts\n * export const uploads = new FileServer('Uploads', { open: ['brand/**'] });\n *\n * uploads.getUploadURL({ path, contentType }) // an S3 presign at the bucket\n * uploads.url('brand/logo.png') // open path — no signature\n * uploads.signedUrl('invoices/7.pdf') // one object, one recipient\n * ```\n *\n * **One construct, one client, both halves.** How a file lands in the bucket is\n * the same whether or not it is served, so splitting the upload half from the\n * serving half would be ceremony rather than safety. The client is a *superset*\n * of `StorageClient`, made literal in the type, so anything taking\n * `services.uploads` keeps compiling when the construct behind it grows a\n * serving half.\n *\n * **The id names the bucket, not the surface.** This is the detail that has to\n * be right before anything ships, because getting it backwards is destructive:\n * the migration people actually perform is serving a bucket they already have,\n * and naming the bucket after the construct makes that edit *add* a node rather\n * than rename the one holding the data.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport { createStorageClient, type StorageClient } from '@geekmidas/storage';\nimport type { Construct, Declarable } from './construct-interface';\n\n/**\n * A key an open pattern admits.\n *\n * The same template-literal trick `ConstructName` uses, so `open` is checked at\n * the call site rather than only at the edge:\n *\n * ```ts\n * files.url('brand/logo.png') // ok\n * files.url(`avatars/${id}.png`) // ok — a template literal still matches\n * files.url('invoices/7.pdf') // not assignable — never served unsigned\n * ```\n *\n * A single star and a double star resolve to the same type, stated rather than\n * discovered: a template literal cannot exclude `/`, so this is a prefix-and-\n * suffix guard while the *exact* pattern is enforced by the bucket policy, by\n * the cache behaviour, and by this construct's own runtime check. The type says\n * plausibly open; the infrastructure says definitely.\n */\nexport type Served<P extends string> = P extends `${infer Head}**`\n\t? `${Head}${string}`\n\t: P extends `${infer Head}*${infer Tail}`\n\t\t? `${Head}${string}${Tail}`\n\t\t: P;\n\n/** What consuming a file server hands you: a bucket client that also serves. */\nexport interface FileServerClient<TOpen extends string = never>\n\textends StorageClient {\n\t/**\n\t * The unsigned address of an open object.\n\t *\n\t * Compile-checked against the declared patterns, *and* checked again at\n\t * runtime — a JavaScript caller gets no compiler, and minting an unsigned\n\t * URL for a private object is a leak rather than a mistake.\n\t *\n\t * @throws {NotOpen} when the key matches no declared pattern.\n\t */\n\turl(key: Served<TOpen>): string;\n\n\t/**\n\t * The same thing for a key the compiler cannot see.\n\t *\n\t * A fully dynamic key does not type, which is correct and is also the case\n\t * people will hit — so the escape hatch is a named method rather than a\n\t * cast, and it runs the *same* runtime check rather than being a way around\n\t * it.\n\t *\n\t * @throws {NotOpen} when the key matches no declared pattern.\n\t */\n\topenUrl(key: string): string;\n\n\t/**\n\t * A signed address for one object and one recipient.\n\t *\n\t * Today this is an **S3 presign at the bucket**, which is what works\n\t * unchanged against MinIO locally and against S3 deployed. It is deliberately\n\t * not described as a CDN URL: CloudFront signing is a different mechanism,\n\t * with different keys, against a different host, and it needs key material\n\t * this construct does not yet declare.\n\t *\n\t * @param expiresIn - minutes, matching `getUploadURL`.\n\t */\n\tsignedUrl(key: string, expiresIn?: number): Promise<string>;\n}\n\nexport interface FileServerOptions<TOpen extends string = never> {\n\t/**\n\t * Paths served without a signature. Everything else needs one.\n\t *\n\t * Private by default: a bucket where forgetting a flag publishes user\n\t * uploads is the wrong default, so this is an exception list, and listing it\n\t * in code means exactly one set of paths is unauthenticated and it is\n\t * reviewable in a diff.\n\t *\n\t * Uploading is deliberately *not* restricted to these. Writing to a path\n\t * that happens to be open is ordinary; the only asymmetry worth enforcing is\n\t * that you cannot hand out an unsigned URL for something the server would\n\t * refuse to serve unsigned.\n\t */\n\topen?: readonly TOpen[];\n\t/** Keep previous versions of an object. */\n\tversioned?: boolean;\n\t/**\n\t * A bucket declared elsewhere, instead of one of this server's own.\n\t *\n\t * `new FileServer('Uploads')` declares both nodes; this fronts a bucket that\n\t * already exists, which is also how the two edges come back when you want\n\t * them separately — a handler depending on the bucket writes, a handler\n\t * depending on the server mints URLs.\n\t */\n\torigin?: Declarable;\n}\n\n/** A key was asked to be served unsigned, and no open pattern admits it. */\nexport class NotOpen extends Error {\n\tconstructor(\n\t\treadonly key: string,\n\t\treadonly open: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`'${key}' is not served without a signature. ` +\n\t\t\t\t(open.length\n\t\t\t\t\t? `Open paths: ${open.join(', ')}. Use signedUrl() for anything else.`\n\t\t\t\t\t: 'This file server declares no open paths, so every object needs signedUrl().'),\n\t\t);\n\t\tthis.name = 'NotOpen';\n\t}\n}\n\nexport class FileServer<\n\tTName extends string = string,\n\tTOpen extends string = never,\n> implements Construct<TName, FileServerClient<TOpen>>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, FileServerClient<TOpen>>;\n\n\t/** The id of the surface node, as opposed to the bucket's. */\n\treadonly surfaceId: string;\n\n\tprivate readonly open: readonly string[];\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the keys the\n\t * target publishes and the keys the client reads cannot drift.\n\t */\n\tprivate readonly keys: { origin: string; server: string };\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly options: FileServerOptions<TOpen> = {},\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.open = options.open ?? [];\n\n\t\t// The surface takes the suffix and the bucket keeps the plain id, which\n\t\t// is what makes \"serve a bucket I already have\" an edit that adds a node\n\t\t// instead of renaming the one holding the data.\n\t\tconst originId = options.origin?.id ?? canonical;\n\t\tthis.surfaceId = `${canonical}Server`;\n\n\t\tthis.keys = {\n\t\t\torigin: provideKey(originId, 'url'),\n\t\t\tserver: provideKey(this.surfaceId, 'url'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (registerOptions) => this.connect(registerOptions),\n\t\t};\n\t}\n\n\t/**\n\t * The served surface, as something an edge can point at.\n\t *\n\t * Depending on the `FileServer` itself points at the **bucket** — its\n\t * canonical node — which is right for a handler: it presigns and writes. But\n\t * a bucket's URL is never public, so a site depending on it gets nothing\n\t * inlined, which is correct and also useless.\n\t *\n\t * This is the other half: `.dependsOn([uploads.server])` points at the\n\t * surface, whose address *is* public, and is what puts\n\t * `VITE_UPLOADS_SERVER_URL` in a bundle. The same accessor shape\n\t * `database.reader()` uses, and for the same reason — one construct owning\n\t * two addresses needs a way to name the second.\n\t */\n\tget server(): Declarable {\n\t\tconst surface = this.declare().find(\n\t\t\t(declaration) => declaration.id === this.surfaceId,\n\t\t);\n\n\t\treturn {\n\t\t\tid: this.surfaceId,\n\t\t\tdeclare: () => (surface ? [surface] : []),\n\t\t};\n\t}\n\n\t/**\n\t * The bucket and the surface over it — or just the surface, when the bucket\n\t * was declared elsewhere.\n\t *\n\t * The surface names its origin through `of`, which is what makes the\n\t * reference check work: an origin resolving to nothing is a build failure,\n\t * the same guarantee `assertDerivations` already gives a reader and a schema\n\t * tenant.\n\t */\n\tdeclare(): Declaration[] {\n\t\tconst origin = this.options.origin;\n\n\t\tconst surface: Declaration = {\n\t\t\tkind: 'file-server',\n\t\t\tid: this.surfaceId,\n\t\t\tof: origin?.id ?? this.id,\n\t\t\t...(this.open.length ? { open: this.open } : {}),\n\t\t\tprovides: [this.keys.server],\n\t\t};\n\n\t\t// A vended bucket is declared here; a given one already declares itself,\n\t\t// and declaring it twice is the duplication the whole model removes.\n\t\tif (origin) return [surface];\n\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'objects',\n\t\t\t\tid: this.id,\n\t\t\t\tprovides: [this.keys.origin],\n\t\t\t\t...(this.options.versioned ? { versioned: true } : {}),\n\t\t\t},\n\t\t\tsurface,\n\t\t];\n\t}\n\n\t/**\n\t * The bucket's client, with the serving half added.\n\t *\n\t * Both addresses are read here, which is also how the environment reaches a\n\t * handler: the sniffer records what this reads, so `.dependsOn([uploads])`\n\t * carries the bucket URL and the server URL without either being written\n\t * down beside the handler.\n\t */\n\tprivate async connect(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<FileServerClient<TOpen>> {\n\t\tconst { origin, server } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\torigin: get(this.keys.origin).string(),\n\t\t\t\tserver: get(this.keys.server).string(),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\tconst storage = createStorageClient(origin);\n\t\tconst base = server.replace(/\\/+$/, '');\n\n\t\tconst serve = (key: string): string => {\n\t\t\tif (!this.serves(key)) throw new NotOpen(key, this.open);\n\n\t\t\treturn `${base}/${key.replace(/^\\/+/, '')}`;\n\t\t};\n\n\t\t// Delegating rather than copying: the bucket half is whatever the driver\n\t\t// produced, including anything it grows later, and only the serving half\n\t\t// is added on top.\n\t\treturn Object.assign(Object.create(storage) as StorageClient, {\n\t\t\turl: serve as (key: Served<TOpen>) => string,\n\t\t\topenUrl: serve,\n\t\t\tsignedUrl: (key: string, expiresIn?: number) =>\n\t\t\t\tstorage.getDownloadURL({ path: key } as never, expiresIn),\n\t\t}) as FileServerClient<TOpen>;\n\t}\n\n\t/**\n\t * Whether an open pattern admits a key.\n\t *\n\t * The runtime half of {@link Served}, and stricter than it: here a single\n\t * star stops at a segment boundary and a double star does not, which is the\n\t * distinction a template-literal type cannot draw and the infrastructure\n\t * does.\n\t */\n\tprivate serves(key: string): boolean {\n\t\treturn this.open.some((pattern) => globToRegExp(pattern).test(key));\n\t}\n}\n\n/**\n * One open pattern as an anchored expression.\n *\n * A double star crosses `/`; a single star does not. Both are consumed in one\n * left-to-right pass rather than by substitution, so a pattern cannot be\n * rewritten into a placeholder that a later pass mistakes for literal text.\n */\nfunction globToRegExp(pattern: string): RegExp {\n\tlet source = '';\n\n\tfor (let index = 0; index < pattern.length; index++) {\n\t\tconst character = pattern[index];\n\n\t\tif (character !== '*') {\n\t\t\tsource += character?.replace(/[.+^${}()|[\\]\\\\?]/, '\\\\$&') ?? '';\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (pattern[index + 1] === '*') {\n\t\t\tsource += '.*';\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tsource += '[^/]*';\n\t}\n\n\treturn new RegExp(`^${source}$`);\n}\n"],"mappings":"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"}
|
package/dist/file-server.d.cts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as Declarable, n as Construct } from "./construct-interface-B1AfqRl4.cjs";
|
|
2
2
|
import { Service } from "@geekmidas/services";
|
|
3
3
|
import { ConstructName, Declaration } from "@geekmidas/manifest";
|
|
4
4
|
import { StorageClient } from "@geekmidas/storage";
|
|
5
|
-
|
|
6
5
|
//#region src/file-server.d.ts
|
|
7
|
-
|
|
8
6
|
/**
|
|
9
7
|
* A key an open pattern admits.
|
|
10
8
|
*
|
|
@@ -23,9 +21,9 @@ import { StorageClient } from "@geekmidas/storage";
|
|
|
23
21
|
* the cache behaviour, and by this construct's own runtime check. The type says
|
|
24
22
|
* plausibly open; the infrastructure says definitely.
|
|
25
23
|
*/
|
|
26
|
-
type Served<P extends string> = P extends `${infer Head}**` ? `${Head}${string}` : P extends `${infer Head}*${infer Tail}` ? `${Head}${string}${Tail}` : P;
|
|
24
|
+
export type Served<P extends string> = P extends `${infer Head}**` ? `${Head}${string}` : P extends `${infer Head}*${infer Tail}` ? `${Head}${string}${Tail}` : P;
|
|
27
25
|
/** What consuming a file server hands you: a bucket client that also serves. */
|
|
28
|
-
interface FileServerClient<TOpen extends string = never> extends StorageClient {
|
|
26
|
+
export interface FileServerClient<TOpen extends string = never> extends StorageClient {
|
|
29
27
|
/**
|
|
30
28
|
* The unsigned address of an open object.
|
|
31
29
|
*
|
|
@@ -60,7 +58,7 @@ interface FileServerClient<TOpen extends string = never> extends StorageClient {
|
|
|
60
58
|
*/
|
|
61
59
|
signedUrl(key: string, expiresIn?: number): Promise<string>;
|
|
62
60
|
}
|
|
63
|
-
interface FileServerOptions<TOpen extends string = never> {
|
|
61
|
+
export interface FileServerOptions<TOpen extends string = never> {
|
|
64
62
|
/**
|
|
65
63
|
* Paths served without a signature. Everything else needs one.
|
|
66
64
|
*
|
|
@@ -88,12 +86,12 @@ interface FileServerOptions<TOpen extends string = never> {
|
|
|
88
86
|
origin?: Declarable;
|
|
89
87
|
}
|
|
90
88
|
/** A key was asked to be served unsigned, and no open pattern admits it. */
|
|
91
|
-
declare class NotOpen extends Error {
|
|
89
|
+
export declare class NotOpen extends Error {
|
|
92
90
|
readonly key: string;
|
|
93
91
|
readonly open: readonly string[];
|
|
94
92
|
constructor(key: string, open: readonly string[]);
|
|
95
93
|
}
|
|
96
|
-
declare class FileServer<TName extends string = string, TOpen extends string = never> implements Construct<TName, FileServerClient<TOpen>> {
|
|
94
|
+
export declare class FileServer<TName extends string = string, TOpen extends string = never> implements Construct<TName, FileServerClient<TOpen>> {
|
|
97
95
|
private readonly options;
|
|
98
96
|
readonly id: TName;
|
|
99
97
|
readonly service: Service<Uncapitalize<TName>, FileServerClient<TOpen>>;
|
|
@@ -150,7 +148,5 @@ declare class FileServer<TName extends string = string, TOpen extends string = n
|
|
|
150
148
|
*/
|
|
151
149
|
private serves;
|
|
152
150
|
}
|
|
153
|
-
//# sourceMappingURL=file-server.d.ts.map
|
|
154
151
|
//#endregion
|
|
155
|
-
export { FileServer, FileServerClient, FileServerOptions, NotOpen, Served };
|
|
156
152
|
//# sourceMappingURL=file-server.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-server.d.cts","names":[],"sources":["../src/file-server.ts"],"
|
|
1
|
+
{"version":3,"file":"file-server.d.cts","names":[],"sources":["../src/file-server.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;YA4DY,OAAO,oBAAoB,mBAAmB,cACpD,kBACH,mBAAmB,cAAc,YAC7B,gBAAgB,SACnB;;iBAGa,iBAAiB,sCACzB;;;;;;;;;;EAUR,IAAI,KAAK,OAAO;;;;;;;;;;;EAYhB,QAAQ;;;;;;;;;;;;EAaR,UAAU,aAAa,qBAAqB;;iBAG5B,kBAAkB;;;;;;;;;;;;;;EAclC,gBAAgB;;EAEhB;;;;;;;;;EASA,SAAS;;;qBAIG,gBAAgB;WAElB;WACA;EAFV,YACU,aACA;;qBAYE,WACZ,+BACA,yCACY,UAAU,OAAO,iBAAiB;mBAkB5B;WAhBT,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ,iBAAiB;;WAGvD;mBAEQ;;;;;mBAMA;EAEjB,YACC,IAAI,cAAc,QACD,UAAS,kBAAkB;;;;;;;;;;;;;;;MAuCzC,UAAU;;;;;;;;;;EAoBd,WAAW;;;;;;;;;UAkCG;;;;;;;;;UAsCN"}
|
package/dist/file-server.d.mts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as Declarable, n as Construct } from "./construct-interface-DluK3_da.mjs";
|
|
2
2
|
import { ConstructName, Declaration } from "@geekmidas/manifest";
|
|
3
3
|
import { StorageClient } from "@geekmidas/storage";
|
|
4
4
|
import { Service } from "@geekmidas/services";
|
|
5
|
-
|
|
6
5
|
//#region src/file-server.d.ts
|
|
7
|
-
|
|
8
6
|
/**
|
|
9
7
|
* A key an open pattern admits.
|
|
10
8
|
*
|
|
@@ -23,9 +21,9 @@ import { Service } from "@geekmidas/services";
|
|
|
23
21
|
* the cache behaviour, and by this construct's own runtime check. The type says
|
|
24
22
|
* plausibly open; the infrastructure says definitely.
|
|
25
23
|
*/
|
|
26
|
-
type Served<P extends string> = P extends `${infer Head}**` ? `${Head}${string}` : P extends `${infer Head}*${infer Tail}` ? `${Head}${string}${Tail}` : P;
|
|
24
|
+
export type Served<P extends string> = P extends `${infer Head}**` ? `${Head}${string}` : P extends `${infer Head}*${infer Tail}` ? `${Head}${string}${Tail}` : P;
|
|
27
25
|
/** What consuming a file server hands you: a bucket client that also serves. */
|
|
28
|
-
interface FileServerClient<TOpen extends string = never> extends StorageClient {
|
|
26
|
+
export interface FileServerClient<TOpen extends string = never> extends StorageClient {
|
|
29
27
|
/**
|
|
30
28
|
* The unsigned address of an open object.
|
|
31
29
|
*
|
|
@@ -60,7 +58,7 @@ interface FileServerClient<TOpen extends string = never> extends StorageClient {
|
|
|
60
58
|
*/
|
|
61
59
|
signedUrl(key: string, expiresIn?: number): Promise<string>;
|
|
62
60
|
}
|
|
63
|
-
interface FileServerOptions<TOpen extends string = never> {
|
|
61
|
+
export interface FileServerOptions<TOpen extends string = never> {
|
|
64
62
|
/**
|
|
65
63
|
* Paths served without a signature. Everything else needs one.
|
|
66
64
|
*
|
|
@@ -88,12 +86,12 @@ interface FileServerOptions<TOpen extends string = never> {
|
|
|
88
86
|
origin?: Declarable;
|
|
89
87
|
}
|
|
90
88
|
/** A key was asked to be served unsigned, and no open pattern admits it. */
|
|
91
|
-
declare class NotOpen extends Error {
|
|
89
|
+
export declare class NotOpen extends Error {
|
|
92
90
|
readonly key: string;
|
|
93
91
|
readonly open: readonly string[];
|
|
94
92
|
constructor(key: string, open: readonly string[]);
|
|
95
93
|
}
|
|
96
|
-
declare class FileServer<TName extends string = string, TOpen extends string = never> implements Construct<TName, FileServerClient<TOpen>> {
|
|
94
|
+
export declare class FileServer<TName extends string = string, TOpen extends string = never> implements Construct<TName, FileServerClient<TOpen>> {
|
|
97
95
|
private readonly options;
|
|
98
96
|
readonly id: TName;
|
|
99
97
|
readonly service: Service<Uncapitalize<TName>, FileServerClient<TOpen>>;
|
|
@@ -150,7 +148,5 @@ declare class FileServer<TName extends string = string, TOpen extends string = n
|
|
|
150
148
|
*/
|
|
151
149
|
private serves;
|
|
152
150
|
}
|
|
153
|
-
//# sourceMappingURL=file-server.d.ts.map
|
|
154
151
|
//#endregion
|
|
155
|
-
export { FileServer, FileServerClient, FileServerOptions, NotOpen, Served };
|
|
156
152
|
//# sourceMappingURL=file-server.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-server.d.mts","names":[],"sources":["../src/file-server.ts"],"
|
|
1
|
+
{"version":3,"file":"file-server.d.mts","names":[],"sources":["../src/file-server.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;YA4DY,OAAO,oBAAoB,mBAAmB,cACpD,kBACH,mBAAmB,cAAc,YAC7B,gBAAgB,SACnB;;iBAGa,iBAAiB,sCACzB;;;;;;;;;;EAUR,IAAI,KAAK,OAAO;;;;;;;;;;;EAYhB,QAAQ;;;;;;;;;;;;EAaR,UAAU,aAAa,qBAAqB;;iBAG5B,kBAAkB;;;;;;;;;;;;;;EAclC,gBAAgB;;EAEhB;;;;;;;;;EASA,SAAS;;;qBAIG,gBAAgB;WAElB;WACA;EAFV,YACU,aACA;;qBAYE,WACZ,+BACA,yCACY,UAAU,OAAO,iBAAiB;mBAkB5B;WAhBT,IAAI;WACJ,SAAS,QAAQ,aAAa,QAAQ,iBAAiB;;WAGvD;mBAEQ;;;;;mBAMA;EAEjB,YACC,IAAI,cAAc,QACD,UAAS,kBAAkB;;;;;;;;;;;;;;;MAuCzC,UAAU;;;;;;;;;;EAoBd,WAAW;;;;;;;;;UAkCG;;;;;;;;;UAsCN"}
|
package/dist/file-server.mjs
CHANGED
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import { canonicalId, provideKey, serviceKey } from "@geekmidas/manifest";
|
|
2
2
|
import { createStorageClient } from "@geekmidas/storage";
|
|
3
|
-
|
|
4
3
|
//#region src/file-server.ts
|
|
4
|
+
/**
|
|
5
|
+
* `FileServer` — a domain that serves a bucket's objects.
|
|
6
|
+
*
|
|
7
|
+
* `ObjectStorage` is the bucket: it writes, it presigns, and its URL is never
|
|
8
|
+
* public. Serving those objects on a domain is a different thing, and it is a
|
|
9
|
+
* construct of its own rather than a flag on the bucket — see
|
|
10
|
+
* `docs/design/constructs-paradigm.md`, "Why a separate construct, and what it
|
|
11
|
+
* costs", for the comparison that rules the other two shapes out.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* export const uploads = new FileServer('Uploads', { open: ['brand/**'] });
|
|
15
|
+
*
|
|
16
|
+
* uploads.getUploadURL({ path, contentType }) // an S3 presign at the bucket
|
|
17
|
+
* uploads.url('brand/logo.png') // open path — no signature
|
|
18
|
+
* uploads.signedUrl('invoices/7.pdf') // one object, one recipient
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* **One construct, one client, both halves.** How a file lands in the bucket is
|
|
22
|
+
* the same whether or not it is served, so splitting the upload half from the
|
|
23
|
+
* serving half would be ceremony rather than safety. The client is a *superset*
|
|
24
|
+
* of `StorageClient`, made literal in the type, so anything taking
|
|
25
|
+
* `services.uploads` keeps compiling when the construct behind it grows a
|
|
26
|
+
* serving half.
|
|
27
|
+
*
|
|
28
|
+
* **The id names the bucket, not the surface.** This is the detail that has to
|
|
29
|
+
* be right before anything ships, because getting it backwards is destructive:
|
|
30
|
+
* the migration people actually perform is serving a bucket they already have,
|
|
31
|
+
* and naming the bucket after the construct makes that edit *add* a node rather
|
|
32
|
+
* than rename the one holding the data.
|
|
33
|
+
*/
|
|
5
34
|
/** A key was asked to be served unsigned, and no open pattern admits it. */
|
|
6
35
|
var NotOpen = class extends Error {
|
|
36
|
+
key;
|
|
37
|
+
open;
|
|
7
38
|
constructor(key, open) {
|
|
8
39
|
super(`'${key}' is not served without a signature. ` + (open.length ? `Open paths: ${open.join(", ")}. Use signedUrl() for anything else.` : "This file server declares no open paths, so every object needs signedUrl()."));
|
|
9
40
|
this.key = key;
|
|
@@ -12,6 +43,7 @@ var NotOpen = class extends Error {
|
|
|
12
43
|
}
|
|
13
44
|
};
|
|
14
45
|
var FileServer = class {
|
|
46
|
+
options;
|
|
15
47
|
id;
|
|
16
48
|
service;
|
|
17
49
|
/** The id of the surface node, as opposed to the bucket's. */
|
|
@@ -146,7 +178,7 @@ function globToRegExp(pattern) {
|
|
|
146
178
|
}
|
|
147
179
|
return new RegExp(`^${source}$`);
|
|
148
180
|
}
|
|
149
|
-
|
|
150
181
|
//#endregion
|
|
151
182
|
export { FileServer, NotOpen };
|
|
183
|
+
|
|
152
184
|
//# sourceMappingURL=file-server.mjs.map
|
package/dist/file-server.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-server.mjs","names":["key: string","open: readonly string[]","id: ConstructName<TName>","options: FileServerOptions<TOpen>","surface: Declaration","options: ServiceRegisterOptions","expiresIn?: number","pattern: string"],"sources":["../src/file-server.ts"],"sourcesContent":["/**\n * `FileServer` — a domain that serves a bucket's objects.\n *\n * `ObjectStorage` is the bucket: it writes, it presigns, and its URL is never\n * public. Serving those objects on a domain is a different thing, and it is a\n * construct of its own rather than a flag on the bucket — see\n * `docs/design/constructs-paradigm.md`, \"Why a separate construct, and what it\n * costs\", for the comparison that rules the other two shapes out.\n *\n * ```ts\n * export const uploads = new FileServer('Uploads', { open: ['brand/**'] });\n *\n * uploads.getUploadURL({ path, contentType }) // an S3 presign at the bucket\n * uploads.url('brand/logo.png') // open path — no signature\n * uploads.signedUrl('invoices/7.pdf') // one object, one recipient\n * ```\n *\n * **One construct, one client, both halves.** How a file lands in the bucket is\n * the same whether or not it is served, so splitting the upload half from the\n * serving half would be ceremony rather than safety. The client is a *superset*\n * of `StorageClient`, made literal in the type, so anything taking\n * `services.uploads` keeps compiling when the construct behind it grows a\n * serving half.\n *\n * **The id names the bucket, not the surface.** This is the detail that has to\n * be right before anything ships, because getting it backwards is destructive:\n * the migration people actually perform is serving a bucket they already have,\n * and naming the bucket after the construct makes that edit *add* a node rather\n * than rename the one holding the data.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport { createStorageClient, type StorageClient } from '@geekmidas/storage';\nimport type { Construct, Declarable } from './construct-interface';\n\n/**\n * A key an open pattern admits.\n *\n * The same template-literal trick `ConstructName` uses, so `open` is checked at\n * the call site rather than only at the edge:\n *\n * ```ts\n * files.url('brand/logo.png') // ok\n * files.url(`avatars/${id}.png`) // ok — a template literal still matches\n * files.url('invoices/7.pdf') // not assignable — never served unsigned\n * ```\n *\n * A single star and a double star resolve to the same type, stated rather than\n * discovered: a template literal cannot exclude `/`, so this is a prefix-and-\n * suffix guard while the *exact* pattern is enforced by the bucket policy, by\n * the cache behaviour, and by this construct's own runtime check. The type says\n * plausibly open; the infrastructure says definitely.\n */\nexport type Served<P extends string> = P extends `${infer Head}**`\n\t? `${Head}${string}`\n\t: P extends `${infer Head}*${infer Tail}`\n\t\t? `${Head}${string}${Tail}`\n\t\t: P;\n\n/** What consuming a file server hands you: a bucket client that also serves. */\nexport interface FileServerClient<TOpen extends string = never>\n\textends StorageClient {\n\t/**\n\t * The unsigned address of an open object.\n\t *\n\t * Compile-checked against the declared patterns, *and* checked again at\n\t * runtime — a JavaScript caller gets no compiler, and minting an unsigned\n\t * URL for a private object is a leak rather than a mistake.\n\t *\n\t * @throws {NotOpen} when the key matches no declared pattern.\n\t */\n\turl(key: Served<TOpen>): string;\n\n\t/**\n\t * The same thing for a key the compiler cannot see.\n\t *\n\t * A fully dynamic key does not type, which is correct and is also the case\n\t * people will hit — so the escape hatch is a named method rather than a\n\t * cast, and it runs the *same* runtime check rather than being a way around\n\t * it.\n\t *\n\t * @throws {NotOpen} when the key matches no declared pattern.\n\t */\n\topenUrl(key: string): string;\n\n\t/**\n\t * A signed address for one object and one recipient.\n\t *\n\t * Today this is an **S3 presign at the bucket**, which is what works\n\t * unchanged against MinIO locally and against S3 deployed. It is deliberately\n\t * not described as a CDN URL: CloudFront signing is a different mechanism,\n\t * with different keys, against a different host, and it needs key material\n\t * this construct does not yet declare.\n\t *\n\t * @param expiresIn - minutes, matching `getUploadURL`.\n\t */\n\tsignedUrl(key: string, expiresIn?: number): Promise<string>;\n}\n\nexport interface FileServerOptions<TOpen extends string = never> {\n\t/**\n\t * Paths served without a signature. Everything else needs one.\n\t *\n\t * Private by default: a bucket where forgetting a flag publishes user\n\t * uploads is the wrong default, so this is an exception list, and listing it\n\t * in code means exactly one set of paths is unauthenticated and it is\n\t * reviewable in a diff.\n\t *\n\t * Uploading is deliberately *not* restricted to these. Writing to a path\n\t * that happens to be open is ordinary; the only asymmetry worth enforcing is\n\t * that you cannot hand out an unsigned URL for something the server would\n\t * refuse to serve unsigned.\n\t */\n\topen?: readonly TOpen[];\n\t/** Keep previous versions of an object. */\n\tversioned?: boolean;\n\t/**\n\t * A bucket declared elsewhere, instead of one of this server's own.\n\t *\n\t * `new FileServer('Uploads')` declares both nodes; this fronts a bucket that\n\t * already exists, which is also how the two edges come back when you want\n\t * them separately — a handler depending on the bucket writes, a handler\n\t * depending on the server mints URLs.\n\t */\n\torigin?: Declarable;\n}\n\n/** A key was asked to be served unsigned, and no open pattern admits it. */\nexport class NotOpen extends Error {\n\tconstructor(\n\t\treadonly key: string,\n\t\treadonly open: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`'${key}' is not served without a signature. ` +\n\t\t\t\t(open.length\n\t\t\t\t\t? `Open paths: ${open.join(', ')}. Use signedUrl() for anything else.`\n\t\t\t\t\t: 'This file server declares no open paths, so every object needs signedUrl().'),\n\t\t);\n\t\tthis.name = 'NotOpen';\n\t}\n}\n\nexport class FileServer<\n\tTName extends string = string,\n\tTOpen extends string = never,\n> implements Construct<TName, FileServerClient<TOpen>>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, FileServerClient<TOpen>>;\n\n\t/** The id of the surface node, as opposed to the bucket's. */\n\treadonly surfaceId: string;\n\n\tprivate readonly open: readonly string[];\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the keys the\n\t * target publishes and the keys the client reads cannot drift.\n\t */\n\tprivate readonly keys: { origin: string; server: string };\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly options: FileServerOptions<TOpen> = {},\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.open = options.open ?? [];\n\n\t\t// The surface takes the suffix and the bucket keeps the plain id, which\n\t\t// is what makes \"serve a bucket I already have\" an edit that adds a node\n\t\t// instead of renaming the one holding the data.\n\t\tconst originId = options.origin?.id ?? canonical;\n\t\tthis.surfaceId = `${canonical}Server`;\n\n\t\tthis.keys = {\n\t\t\torigin: provideKey(originId, 'url'),\n\t\t\tserver: provideKey(this.surfaceId, 'url'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (registerOptions) => this.connect(registerOptions),\n\t\t};\n\t}\n\n\t/**\n\t * The served surface, as something an edge can point at.\n\t *\n\t * Depending on the `FileServer` itself points at the **bucket** — its\n\t * canonical node — which is right for a handler: it presigns and writes. But\n\t * a bucket's URL is never public, so a site depending on it gets nothing\n\t * inlined, which is correct and also useless.\n\t *\n\t * This is the other half: `.dependsOn([uploads.server])` points at the\n\t * surface, whose address *is* public, and is what puts\n\t * `VITE_UPLOADS_SERVER_URL` in a bundle. The same accessor shape\n\t * `database.reader()` uses, and for the same reason — one construct owning\n\t * two addresses needs a way to name the second.\n\t */\n\tget server(): Declarable {\n\t\tconst surface = this.declare().find(\n\t\t\t(declaration) => declaration.id === this.surfaceId,\n\t\t);\n\n\t\treturn {\n\t\t\tid: this.surfaceId,\n\t\t\tdeclare: () => (surface ? [surface] : []),\n\t\t};\n\t}\n\n\t/**\n\t * The bucket and the surface over it — or just the surface, when the bucket\n\t * was declared elsewhere.\n\t *\n\t * The surface names its origin through `of`, which is what makes the\n\t * reference check work: an origin resolving to nothing is a build failure,\n\t * the same guarantee `assertDerivations` already gives a reader and a schema\n\t * tenant.\n\t */\n\tdeclare(): Declaration[] {\n\t\tconst origin = this.options.origin;\n\n\t\tconst surface: Declaration = {\n\t\t\tkind: 'file-server',\n\t\t\tid: this.surfaceId,\n\t\t\tof: origin?.id ?? this.id,\n\t\t\t...(this.open.length ? { open: this.open } : {}),\n\t\t\tprovides: [this.keys.server],\n\t\t};\n\n\t\t// A vended bucket is declared here; a given one already declares itself,\n\t\t// and declaring it twice is the duplication the whole model removes.\n\t\tif (origin) return [surface];\n\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'objects',\n\t\t\t\tid: this.id,\n\t\t\t\tprovides: [this.keys.origin],\n\t\t\t\t...(this.options.versioned ? { versioned: true } : {}),\n\t\t\t},\n\t\t\tsurface,\n\t\t];\n\t}\n\n\t/**\n\t * The bucket's client, with the serving half added.\n\t *\n\t * Both addresses are read here, which is also how the environment reaches a\n\t * handler: the sniffer records what this reads, so `.dependsOn([uploads])`\n\t * carries the bucket URL and the server URL without either being written\n\t * down beside the handler.\n\t */\n\tprivate async connect(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<FileServerClient<TOpen>> {\n\t\tconst { origin, server } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\torigin: get(this.keys.origin).string(),\n\t\t\t\tserver: get(this.keys.server).string(),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\tconst storage = createStorageClient(origin);\n\t\tconst base = server.replace(/\\/+$/, '');\n\n\t\tconst serve = (key: string): string => {\n\t\t\tif (!this.serves(key)) throw new NotOpen(key, this.open);\n\n\t\t\treturn `${base}/${key.replace(/^\\/+/, '')}`;\n\t\t};\n\n\t\t// Delegating rather than copying: the bucket half is whatever the driver\n\t\t// produced, including anything it grows later, and only the serving half\n\t\t// is added on top.\n\t\treturn Object.assign(Object.create(storage) as StorageClient, {\n\t\t\turl: serve as (key: Served<TOpen>) => string,\n\t\t\topenUrl: serve,\n\t\t\tsignedUrl: (key: string, expiresIn?: number) =>\n\t\t\t\tstorage.getDownloadURL({ path: key } as never, expiresIn),\n\t\t}) as FileServerClient<TOpen>;\n\t}\n\n\t/**\n\t * Whether an open pattern admits a key.\n\t *\n\t * The runtime half of {@link Served}, and stricter than it: here a single\n\t * star stops at a segment boundary and a double star does not, which is the\n\t * distinction a template-literal type cannot draw and the infrastructure\n\t * does.\n\t */\n\tprivate serves(key: string): boolean {\n\t\treturn this.open.some((pattern) => globToRegExp(pattern).test(key));\n\t}\n}\n\n/**\n * One open pattern as an anchored expression.\n *\n * A double star crosses `/`; a single star does not. Both are consumed in one\n * left-to-right pass rather than by substitution, so a pattern cannot be\n * rewritten into a placeholder that a later pass mistakes for literal text.\n */\nfunction globToRegExp(pattern: string): RegExp {\n\tlet source = '';\n\n\tfor (let index = 0; index < pattern.length; index++) {\n\t\tconst character = pattern[index];\n\n\t\tif (character !== '*') {\n\t\t\tsource += character?.replace(/[.+^${}()|[\\]\\\\?]/, '\\\\$&') ?? '';\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (pattern[index + 1] === '*') {\n\t\t\tsource += '.*';\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tsource += '[^/]*';\n\t}\n\n\treturn new RegExp(`^${source}$`);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-server.mjs","names":[],"sources":["../src/file-server.ts"],"sourcesContent":["/**\n * `FileServer` — a domain that serves a bucket's objects.\n *\n * `ObjectStorage` is the bucket: it writes, it presigns, and its URL is never\n * public. Serving those objects on a domain is a different thing, and it is a\n * construct of its own rather than a flag on the bucket — see\n * `docs/design/constructs-paradigm.md`, \"Why a separate construct, and what it\n * costs\", for the comparison that rules the other two shapes out.\n *\n * ```ts\n * export const uploads = new FileServer('Uploads', { open: ['brand/**'] });\n *\n * uploads.getUploadURL({ path, contentType }) // an S3 presign at the bucket\n * uploads.url('brand/logo.png') // open path — no signature\n * uploads.signedUrl('invoices/7.pdf') // one object, one recipient\n * ```\n *\n * **One construct, one client, both halves.** How a file lands in the bucket is\n * the same whether or not it is served, so splitting the upload half from the\n * serving half would be ceremony rather than safety. The client is a *superset*\n * of `StorageClient`, made literal in the type, so anything taking\n * `services.uploads` keeps compiling when the construct behind it grows a\n * serving half.\n *\n * **The id names the bucket, not the surface.** This is the detail that has to\n * be right before anything ships, because getting it backwards is destructive:\n * the migration people actually perform is serving a bucket they already have,\n * and naming the bucket after the construct makes that edit *add* a node rather\n * than rename the one holding the data.\n */\n\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport { createStorageClient, type StorageClient } from '@geekmidas/storage';\nimport type { Construct, Declarable } from './construct-interface';\n\n/**\n * A key an open pattern admits.\n *\n * The same template-literal trick `ConstructName` uses, so `open` is checked at\n * the call site rather than only at the edge:\n *\n * ```ts\n * files.url('brand/logo.png') // ok\n * files.url(`avatars/${id}.png`) // ok — a template literal still matches\n * files.url('invoices/7.pdf') // not assignable — never served unsigned\n * ```\n *\n * A single star and a double star resolve to the same type, stated rather than\n * discovered: a template literal cannot exclude `/`, so this is a prefix-and-\n * suffix guard while the *exact* pattern is enforced by the bucket policy, by\n * the cache behaviour, and by this construct's own runtime check. The type says\n * plausibly open; the infrastructure says definitely.\n */\nexport type Served<P extends string> = P extends `${infer Head}**`\n\t? `${Head}${string}`\n\t: P extends `${infer Head}*${infer Tail}`\n\t\t? `${Head}${string}${Tail}`\n\t\t: P;\n\n/** What consuming a file server hands you: a bucket client that also serves. */\nexport interface FileServerClient<TOpen extends string = never>\n\textends StorageClient {\n\t/**\n\t * The unsigned address of an open object.\n\t *\n\t * Compile-checked against the declared patterns, *and* checked again at\n\t * runtime — a JavaScript caller gets no compiler, and minting an unsigned\n\t * URL for a private object is a leak rather than a mistake.\n\t *\n\t * @throws {NotOpen} when the key matches no declared pattern.\n\t */\n\turl(key: Served<TOpen>): string;\n\n\t/**\n\t * The same thing for a key the compiler cannot see.\n\t *\n\t * A fully dynamic key does not type, which is correct and is also the case\n\t * people will hit — so the escape hatch is a named method rather than a\n\t * cast, and it runs the *same* runtime check rather than being a way around\n\t * it.\n\t *\n\t * @throws {NotOpen} when the key matches no declared pattern.\n\t */\n\topenUrl(key: string): string;\n\n\t/**\n\t * A signed address for one object and one recipient.\n\t *\n\t * Today this is an **S3 presign at the bucket**, which is what works\n\t * unchanged against MinIO locally and against S3 deployed. It is deliberately\n\t * not described as a CDN URL: CloudFront signing is a different mechanism,\n\t * with different keys, against a different host, and it needs key material\n\t * this construct does not yet declare.\n\t *\n\t * @param expiresIn - minutes, matching `getUploadURL`.\n\t */\n\tsignedUrl(key: string, expiresIn?: number): Promise<string>;\n}\n\nexport interface FileServerOptions<TOpen extends string = never> {\n\t/**\n\t * Paths served without a signature. Everything else needs one.\n\t *\n\t * Private by default: a bucket where forgetting a flag publishes user\n\t * uploads is the wrong default, so this is an exception list, and listing it\n\t * in code means exactly one set of paths is unauthenticated and it is\n\t * reviewable in a diff.\n\t *\n\t * Uploading is deliberately *not* restricted to these. Writing to a path\n\t * that happens to be open is ordinary; the only asymmetry worth enforcing is\n\t * that you cannot hand out an unsigned URL for something the server would\n\t * refuse to serve unsigned.\n\t */\n\topen?: readonly TOpen[];\n\t/** Keep previous versions of an object. */\n\tversioned?: boolean;\n\t/**\n\t * A bucket declared elsewhere, instead of one of this server's own.\n\t *\n\t * `new FileServer('Uploads')` declares both nodes; this fronts a bucket that\n\t * already exists, which is also how the two edges come back when you want\n\t * them separately — a handler depending on the bucket writes, a handler\n\t * depending on the server mints URLs.\n\t */\n\torigin?: Declarable;\n}\n\n/** A key was asked to be served unsigned, and no open pattern admits it. */\nexport class NotOpen extends Error {\n\tconstructor(\n\t\treadonly key: string,\n\t\treadonly open: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`'${key}' is not served without a signature. ` +\n\t\t\t\t(open.length\n\t\t\t\t\t? `Open paths: ${open.join(', ')}. Use signedUrl() for anything else.`\n\t\t\t\t\t: 'This file server declares no open paths, so every object needs signedUrl().'),\n\t\t);\n\t\tthis.name = 'NotOpen';\n\t}\n}\n\nexport class FileServer<\n\tTName extends string = string,\n\tTOpen extends string = never,\n> implements Construct<TName, FileServerClient<TOpen>>\n{\n\treadonly id: TName;\n\treadonly service: Service<Uncapitalize<TName>, FileServerClient<TOpen>>;\n\n\t/** The id of the surface node, as opposed to the bucket's. */\n\treadonly surfaceId: string;\n\n\tprivate readonly open: readonly string[];\n\n\t/**\n\t * Declared once and read by both `declare()` and `connect()`, so the keys the\n\t * target publishes and the keys the client reads cannot drift.\n\t */\n\tprivate readonly keys: { origin: string; server: string };\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly options: FileServerOptions<TOpen> = {},\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\n\t\tthis.id = canonical as TName;\n\t\tthis.open = options.open ?? [];\n\n\t\t// The surface takes the suffix and the bucket keeps the plain id, which\n\t\t// is what makes \"serve a bucket I already have\" an edit that adds a node\n\t\t// instead of renaming the one holding the data.\n\t\tconst originId = options.origin?.id ?? canonical;\n\t\tthis.surfaceId = `${canonical}Server`;\n\n\t\tthis.keys = {\n\t\t\torigin: provideKey(originId, 'url'),\n\t\t\tserver: provideKey(this.surfaceId, 'url'),\n\t\t};\n\n\t\t// A field, not a getter: consumers cache services by object identity.\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tregister: (registerOptions) => this.connect(registerOptions),\n\t\t};\n\t}\n\n\t/**\n\t * The served surface, as something an edge can point at.\n\t *\n\t * Depending on the `FileServer` itself points at the **bucket** — its\n\t * canonical node — which is right for a handler: it presigns and writes. But\n\t * a bucket's URL is never public, so a site depending on it gets nothing\n\t * inlined, which is correct and also useless.\n\t *\n\t * This is the other half: `.dependsOn([uploads.server])` points at the\n\t * surface, whose address *is* public, and is what puts\n\t * `VITE_UPLOADS_SERVER_URL` in a bundle. The same accessor shape\n\t * `database.reader()` uses, and for the same reason — one construct owning\n\t * two addresses needs a way to name the second.\n\t */\n\tget server(): Declarable {\n\t\tconst surface = this.declare().find(\n\t\t\t(declaration) => declaration.id === this.surfaceId,\n\t\t);\n\n\t\treturn {\n\t\t\tid: this.surfaceId,\n\t\t\tdeclare: () => (surface ? [surface] : []),\n\t\t};\n\t}\n\n\t/**\n\t * The bucket and the surface over it — or just the surface, when the bucket\n\t * was declared elsewhere.\n\t *\n\t * The surface names its origin through `of`, which is what makes the\n\t * reference check work: an origin resolving to nothing is a build failure,\n\t * the same guarantee `assertDerivations` already gives a reader and a schema\n\t * tenant.\n\t */\n\tdeclare(): Declaration[] {\n\t\tconst origin = this.options.origin;\n\n\t\tconst surface: Declaration = {\n\t\t\tkind: 'file-server',\n\t\t\tid: this.surfaceId,\n\t\t\tof: origin?.id ?? this.id,\n\t\t\t...(this.open.length ? { open: this.open } : {}),\n\t\t\tprovides: [this.keys.server],\n\t\t};\n\n\t\t// A vended bucket is declared here; a given one already declares itself,\n\t\t// and declaring it twice is the duplication the whole model removes.\n\t\tif (origin) return [surface];\n\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'objects',\n\t\t\t\tid: this.id,\n\t\t\t\tprovides: [this.keys.origin],\n\t\t\t\t...(this.options.versioned ? { versioned: true } : {}),\n\t\t\t},\n\t\t\tsurface,\n\t\t];\n\t}\n\n\t/**\n\t * The bucket's client, with the serving half added.\n\t *\n\t * Both addresses are read here, which is also how the environment reaches a\n\t * handler: the sniffer records what this reads, so `.dependsOn([uploads])`\n\t * carries the bucket URL and the server URL without either being written\n\t * down beside the handler.\n\t */\n\tprivate async connect(\n\t\toptions: ServiceRegisterOptions,\n\t): Promise<FileServerClient<TOpen>> {\n\t\tconst { origin, server } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\torigin: get(this.keys.origin).string(),\n\t\t\t\tserver: get(this.keys.server).string(),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\tconst storage = createStorageClient(origin);\n\t\tconst base = server.replace(/\\/+$/, '');\n\n\t\tconst serve = (key: string): string => {\n\t\t\tif (!this.serves(key)) throw new NotOpen(key, this.open);\n\n\t\t\treturn `${base}/${key.replace(/^\\/+/, '')}`;\n\t\t};\n\n\t\t// Delegating rather than copying: the bucket half is whatever the driver\n\t\t// produced, including anything it grows later, and only the serving half\n\t\t// is added on top.\n\t\treturn Object.assign(Object.create(storage) as StorageClient, {\n\t\t\turl: serve as (key: Served<TOpen>) => string,\n\t\t\topenUrl: serve,\n\t\t\tsignedUrl: (key: string, expiresIn?: number) =>\n\t\t\t\tstorage.getDownloadURL({ path: key } as never, expiresIn),\n\t\t}) as FileServerClient<TOpen>;\n\t}\n\n\t/**\n\t * Whether an open pattern admits a key.\n\t *\n\t * The runtime half of {@link Served}, and stricter than it: here a single\n\t * star stops at a segment boundary and a double star does not, which is the\n\t * distinction a template-literal type cannot draw and the infrastructure\n\t * does.\n\t */\n\tprivate serves(key: string): boolean {\n\t\treturn this.open.some((pattern) => globToRegExp(pattern).test(key));\n\t}\n}\n\n/**\n * One open pattern as an anchored expression.\n *\n * A double star crosses `/`; a single star does not. Both are consumed in one\n * left-to-right pass rather than by substitution, so a pattern cannot be\n * rewritten into a placeholder that a later pass mistakes for literal text.\n */\nfunction globToRegExp(pattern: string): RegExp {\n\tlet source = '';\n\n\tfor (let index = 0; index < pattern.length; index++) {\n\t\tconst character = pattern[index];\n\n\t\tif (character !== '*') {\n\t\t\tsource += character?.replace(/[.+^${}()|[\\]\\\\?]/, '\\\\$&') ?? '';\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (pattern[index + 1] === '*') {\n\t\t\tsource += '.*';\n\t\t\tindex++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tsource += '[^/]*';\n\t}\n\n\treturn new RegExp(`^${source}$`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuIA,IAAa,UAAb,cAA6B,MAAM;CAExB;CACA;CAFV,YACC,KACA,MACC;EACD,MACC,IAAI,IAAI,0CACN,KAAK,SACH,eAAe,KAAK,KAAK,IAAI,EAAE,wCAC/B,8EACL;EARS,KAAA,MAAA;EACA,KAAA,OAAA;EAQT,KAAK,OAAO;CACb;AACD;AAEA,IAAa,aAAb,MAIA;CAiBmB;CAhBlB;CACA;;CAGA;CAEA;;;;;CAMA;CAEA,YACC,IACA,UAAqD,CAAC,GACrD;EADgB,KAAA,UAAA;EAEjB,MAAM,YAAY,YAAY,EAAY;EAE1C,KAAK,KAAK;EACV,KAAK,OAAO,QAAQ,QAAQ,CAAC;EAK7B,MAAM,WAAW,QAAQ,QAAQ,MAAM;EACvC,KAAK,YAAY,GAAG,UAAU;EAE9B,KAAK,OAAO;GACX,QAAQ,WAAW,UAAU,KAAK;GAClC,QAAQ,WAAW,KAAK,WAAW,KAAK;EACzC;EAGA,KAAK,UAAU;GACd,aAAa,WAAW,SAAS;GACjC,WAAW,oBAAoB,KAAK,QAAQ,eAAe;EAC5D;CACD;;;;;;;;;;;;;;;CAgBA,IAAI,SAAqB;EACxB,MAAM,UAAU,KAAK,QAAQ,CAAC,CAAC,MAC7B,gBAAgB,YAAY,OAAO,KAAK,SAC1C;EAEA,OAAO;GACN,IAAI,KAAK;GACT,eAAgB,UAAU,CAAC,OAAO,IAAI,CAAC;EACxC;CACD;;;;;;;;;;CAWA,UAAyB;EACxB,MAAM,SAAS,KAAK,QAAQ;EAE5B,MAAM,UAAuB;GAC5B,MAAM;GACN,IAAI,KAAK;GACT,IAAI,QAAQ,MAAM,KAAK;GACvB,GAAI,KAAK,KAAK,SAAS,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;GAC9C,UAAU,CAAC,KAAK,KAAK,MAAM;EAC5B;EAIA,IAAI,QAAQ,OAAO,CAAC,OAAO;EAE3B,OAAO,CACN;GACC,MAAM;GACN,IAAI,KAAK;GACT,UAAU,CAAC,KAAK,KAAK,MAAM;GAC3B,GAAI,KAAK,QAAQ,YAAY,EAAE,WAAW,KAAK,IAAI,CAAC;EACrD,GACA,OACD;CACD;;;;;;;;;CAUA,MAAc,QACb,SACmC;EACnC,MAAM,EAAE,QAAQ,WAAW,QAAQ,UACjC,QAAQ,SAAS;GACjB,QAAQ,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,OAAO;GACrC,QAAQ,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,OAAO;EACtC,EAAE,CAAC,CACF,MAAM;EAER,MAAM,UAAU,oBAAoB,MAAM;EAC1C,MAAM,OAAO,OAAO,QAAQ,QAAQ,EAAE;EAEtC,MAAM,SAAS,QAAwB;GACtC,IAAI,CAAC,KAAK,OAAO,GAAG,GAAG,MAAM,IAAI,QAAQ,KAAK,KAAK,IAAI;GAEvD,OAAO,GAAG,KAAK,GAAG,IAAI,QAAQ,QAAQ,EAAE;EACzC;EAKA,OAAO,OAAO,OAAO,OAAO,OAAO,OAAO,GAAoB;GAC7D,KAAK;GACL,SAAS;GACT,YAAY,KAAa,cACxB,QAAQ,eAAe,EAAE,MAAM,IAAI,GAAY,SAAS;EAC1D,CAAC;CACF;;;;;;;;;CAUA,OAAe,KAAsB;EACpC,OAAO,KAAK,KAAK,MAAM,YAAY,aAAa,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC;CACnE;AACD;;;;;;;;AASA,SAAS,aAAa,SAAyB;CAC9C,IAAI,SAAS;CAEb,KAAK,IAAI,QAAQ,GAAG,QAAQ,QAAQ,QAAQ,SAAS;EACpD,MAAM,YAAY,QAAQ;EAE1B,IAAI,cAAc,KAAK;GACtB,UAAU,WAAW,QAAQ,qBAAqB,MAAM,KAAK;GAC7D;EACD;EAEA,IAAI,QAAQ,QAAQ,OAAO,KAAK;GAC/B,UAAU;GACV;GACA;EACD;EAEA,UAAU;CACX;CAEA,OAAO,IAAI,OAAO,IAAI,OAAO,EAAE;AAChC"}
|