nucleus-core-ts 0.10.4 → 0.10.5
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/dist/.build-ok
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.10.
|
|
1
|
+
0.10.5
|
package/dist/index.js
CHANGED
|
@@ -606,4 +606,4 @@ ${content}
|
|
|
606
606
|
<script id="api-reference" data-url="${forHtml(documentUrl)}"></script>
|
|
607
607
|
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
608
608
|
</body>
|
|
609
|
-
</html>`}function chainGuards(...guards){return async(context)=>{for(let guard5 of guards){let early=await guard5(context);if(early!==void 0)return early}return}}function docsGroup(groups,config){let swagger=config.swagger;if(swagger?.enabled===!1)return null;let base=swagger?.path??"/swagger",info=swagger?.documentation?.info,title=info?.title??"Nucleus API",description=info?.description??"Auto-generated API documentation",document2=buildOpenApiDocument(groups,swagger),page=viewerHtml(`${base}/json`,title,description);return{prefix:base,routes:[defineRoute({method:"GET",path:"/",detail:{summary:"API documentation",tags:["Documentation"]},handler:()=>new Response(page,{headers:{"content-type":"text/html; charset=utf8"}})}),defineRoute({method:"GET",path:"/json",detail:{summary:"OpenAPI document",tags:["Documentation"]},handler:()=>document2})]}}async function createNucleusServer(options){await buildNucleus(options.config);let groups=getMountedRouteGroups();if(groups.length===0)throw Error("createNucleusServer: the plugin mounted no route groups. That is a wiring "+"fault, not an empty install \u2014 refusing to serve rather than answering 404 to everything.");let resolved=getResolvedOptions2(),rawOptions=options.config.options,securityOf=(candidate)=>typeof candidate==="object"&&candidate!==null?candidate.security?.maxRequestBodyBytes:void 0,maxRequestBodyBytes=securityOf(resolved)??securityOf(rawOptions),storageCeiling=requiredBodyCeiling({storageEnabled:resolved?.storage?.enabled,maxFileSizeBytes:resolved?.storage?.maxFileSizeBytes,maxFiles:resolved?.storage?.formData?.maxFiles}),bunBodyCeiling=bunBodyCeilingFor({storageRequired:storageCeiling,guardCeiling:maxRequestBodyBytes});for(let step of getBootSteps())await step({config:{serve:{maxRequestBodySize:bunBodyCeiling}}});let hostRoutes=options.routes??[],docs=docsGroup([...groups,...hostRoutes],
|
|
609
|
+
</html>`}function chainGuards(...guards){return async(context)=>{for(let guard5 of guards){let early=await guard5(context);if(early!==void 0)return early}return}}function docsGroup(groups,config){let swagger=config.swagger;if(swagger?.enabled===!1)return null;let base=swagger?.path??"/swagger",info=swagger?.documentation?.info,title=info?.title??"Nucleus API",description=info?.description??"Auto-generated API documentation",document2=buildOpenApiDocument(groups,swagger),page=viewerHtml(`${base}/json`,title,description);return{prefix:base,routes:[defineRoute({method:"GET",path:"/",detail:{summary:"API documentation",tags:["Documentation"]},handler:()=>new Response(page,{headers:{"content-type":"text/html; charset=utf8"}})}),defineRoute({method:"GET",path:"/json",detail:{summary:"OpenAPI document",tags:["Documentation"]},handler:()=>document2})]}}async function createNucleusServer(options){await buildNucleus(options.config);let groups=getMountedRouteGroups();if(groups.length===0)throw Error("createNucleusServer: the plugin mounted no route groups. That is a wiring "+"fault, not an empty install \u2014 refusing to serve rather than answering 404 to everything.");let resolved=getResolvedOptions2(),rawOptions=options.config.options,securityOf=(candidate)=>typeof candidate==="object"&&candidate!==null?candidate.security?.maxRequestBodyBytes:void 0,maxRequestBodyBytes=securityOf(resolved)??securityOf(rawOptions),storageCeiling=requiredBodyCeiling({storageEnabled:resolved?.storage?.enabled,maxFileSizeBytes:resolved?.storage?.maxFileSizeBytes,maxFiles:resolved?.storage?.formData?.maxFiles}),bunBodyCeiling=bunBodyCeilingFor({storageRequired:storageCeiling,guardCeiling:maxRequestBodyBytes});for(let step of getBootSteps())await step({config:{serve:{maxRequestBodySize:bunBodyCeiling}}});let hostRoutes=options.routes??[],resolvedSwagger=resolved?.swagger,outerSwagger=options.config.swagger,docs=docsGroup([...groups,...hostRoutes],{swagger:outerSwagger??resolvedSwagger});return serve({groups:[...groups,...docs?[docs]:[],...hostRoutes],maxRequestBodyBytes:bunBodyCeiling,onStop:[...getShutdownSteps(),...options.onStop??[]],socket:options.socket??getMountedSocket()??void 0,staticAssets:(()=>{let directory=getMountedStaticAssets();return directory?{prefix:"/nucleus-core",directory}:void 0})(),onRequest:chainGuards(...options.onRequest?[options.onRequest]:[],createInboundGuard({maxRequestBodyBytes}),getAuthMiddleware()),onError:getErrorHandler(),onAfter:getAfterHandler()??void 0,context:(()=>{let runtime=getMountedRuntime2();return runtime?{nucleus:runtime}:void 0})(),port:options.port,hostname:options.hostname})}init_AzureEmailService();init_Gmail();export{useUpload,usePubSubStore,usePubSub,uploadWithProgress,uploadPercent,uploadFraction,serverFetch,secondsRemaining,requiredBodyCeiling,parseCron2 as parseCron,matchesCron,isDue,generateVerificationEndpoints,generateTenantEndpoints,generateSystemTableEndpoints,generateMonitoringEndpoints,generateMarketplaceEndpoints,generateEndpointsFromConfig,generateDomainEndpoints,generateCohortEndpoints,generateChatEndpoints,generateAuthEndpoints,generateAllEndpoints,generateAdminEndpoints,describeCron,decodeHeaderList,createServerFactory,createRateWindow,createNucleusServer,createApiHook,buildProgress,buildNucleus,VERIFICATION_ENDPOINTS,TENANT_ENDPOINTS,ServerFetch,SYSTEM_TABLES,PAYMENT_ENDPOINTS,MONITORING_ENDPOINTS,MARKETPLACE_ENDPOINTS,GmailService,DOMAIN_ENDPOINTS,CONFIG_ENDPOINTS,COHORT_ENDPOINTS,CHAT_ENDPOINTS,AzureEmailService,AUTH_ENDPOINT_CONFIGS,AUTH_ENDPOINTS};
|
package/dist/server.js
CHANGED
|
@@ -606,4 +606,4 @@ ${content}
|
|
|
606
606
|
<script id="api-reference" data-url="${forHtml(documentUrl)}"></script>
|
|
607
607
|
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
608
608
|
</body>
|
|
609
|
-
</html>`}function chainGuards(...guards){return async(context)=>{for(let guard5 of guards){let early=await guard5(context);if(early!==void 0)return early}return}}function docsGroup(groups,config){let swagger=config.swagger;if(swagger?.enabled===!1)return null;let base=swagger?.path??"/swagger",info=swagger?.documentation?.info,title=info?.title??"Nucleus API",description=info?.description??"Auto-generated API documentation",document=buildOpenApiDocument(groups,swagger),page=viewerHtml(`${base}/json`,title,description);return{prefix:base,routes:[defineRoute({method:"GET",path:"/",detail:{summary:"API documentation",tags:["Documentation"]},handler:()=>new Response(page,{headers:{"content-type":"text/html; charset=utf8"}})}),defineRoute({method:"GET",path:"/json",detail:{summary:"OpenAPI document",tags:["Documentation"]},handler:()=>document})]}}async function createNucleusServer(options){await buildNucleus(options.config);let groups=getMountedRouteGroups();if(groups.length===0)throw Error("createNucleusServer: the plugin mounted no route groups. That is a wiring "+"fault, not an empty install \u2014 refusing to serve rather than answering 404 to everything.");let resolved=getResolvedOptions2(),rawOptions=options.config.options,securityOf=(candidate)=>typeof candidate==="object"&&candidate!==null?candidate.security?.maxRequestBodyBytes:void 0,maxRequestBodyBytes=securityOf(resolved)??securityOf(rawOptions),storageCeiling=requiredBodyCeiling({storageEnabled:resolved?.storage?.enabled,maxFileSizeBytes:resolved?.storage?.maxFileSizeBytes,maxFiles:resolved?.storage?.formData?.maxFiles}),bunBodyCeiling=bunBodyCeilingFor({storageRequired:storageCeiling,guardCeiling:maxRequestBodyBytes});for(let step of getBootSteps())await step({config:{serve:{maxRequestBodySize:bunBodyCeiling}}});let hostRoutes=options.routes??[],docs=docsGroup([...groups,...hostRoutes],
|
|
609
|
+
</html>`}function chainGuards(...guards){return async(context)=>{for(let guard5 of guards){let early=await guard5(context);if(early!==void 0)return early}return}}function docsGroup(groups,config){let swagger=config.swagger;if(swagger?.enabled===!1)return null;let base=swagger?.path??"/swagger",info=swagger?.documentation?.info,title=info?.title??"Nucleus API",description=info?.description??"Auto-generated API documentation",document=buildOpenApiDocument(groups,swagger),page=viewerHtml(`${base}/json`,title,description);return{prefix:base,routes:[defineRoute({method:"GET",path:"/",detail:{summary:"API documentation",tags:["Documentation"]},handler:()=>new Response(page,{headers:{"content-type":"text/html; charset=utf8"}})}),defineRoute({method:"GET",path:"/json",detail:{summary:"OpenAPI document",tags:["Documentation"]},handler:()=>document})]}}async function createNucleusServer(options){await buildNucleus(options.config);let groups=getMountedRouteGroups();if(groups.length===0)throw Error("createNucleusServer: the plugin mounted no route groups. That is a wiring "+"fault, not an empty install \u2014 refusing to serve rather than answering 404 to everything.");let resolved=getResolvedOptions2(),rawOptions=options.config.options,securityOf=(candidate)=>typeof candidate==="object"&&candidate!==null?candidate.security?.maxRequestBodyBytes:void 0,maxRequestBodyBytes=securityOf(resolved)??securityOf(rawOptions),storageCeiling=requiredBodyCeiling({storageEnabled:resolved?.storage?.enabled,maxFileSizeBytes:resolved?.storage?.maxFileSizeBytes,maxFiles:resolved?.storage?.formData?.maxFiles}),bunBodyCeiling=bunBodyCeilingFor({storageRequired:storageCeiling,guardCeiling:maxRequestBodyBytes});for(let step of getBootSteps())await step({config:{serve:{maxRequestBodySize:bunBodyCeiling}}});let hostRoutes=options.routes??[],resolvedSwagger=resolved?.swagger,outerSwagger=options.config.swagger,docs=docsGroup([...groups,...hostRoutes],{swagger:outerSwagger??resolvedSwagger});return serve({groups:[...groups,...docs?[docs]:[],...hostRoutes],maxRequestBodyBytes:bunBodyCeiling,onStop:[...getShutdownSteps(),...options.onStop??[]],socket:options.socket??getMountedSocket()??void 0,staticAssets:(()=>{let directory=getMountedStaticAssets();return directory?{prefix:"/nucleus-core",directory}:void 0})(),onRequest:chainGuards(...options.onRequest?[options.onRequest]:[],createInboundGuard({maxRequestBodyBytes}),getAuthMiddleware()),onError:getErrorHandler(),onAfter:getAfterHandler()??void 0,context:(()=>{let runtime=getMountedRuntime2();return runtime?{nucleus:runtime}:void 0})(),port:options.port,hostname:options.hostname})}init_routeCollector();init_websocket();export{serve,defineWebSocketRoute,defineRoute,decodeHeaderList,createNucleusServer,collectRoutes,buildOpenApiDocument,buildNucleus};
|
|
@@ -3,6 +3,10 @@ interface RefreshResult {
|
|
|
3
3
|
accessToken: string;
|
|
4
4
|
setCookieHeaders: string[];
|
|
5
5
|
}
|
|
6
|
+
/** How long a definitive failure suppresses another attempt. */
|
|
7
|
+
export declare const REFRESH_FAILURE_MEMORY_MS = 5000;
|
|
8
|
+
/** Exposed for tests; also called when a refresh succeeds. */
|
|
9
|
+
export declare function clearRefreshFailure(dedupeKey: string): void;
|
|
6
10
|
export declare function deduplicatedRefresh(refreshConfig: TokenRefreshConfig, refreshToken: string, logger: {
|
|
7
11
|
info: (...args: unknown[]) => void;
|
|
8
12
|
warn: (...args: unknown[]) => void;
|
|
@@ -117,6 +117,32 @@ async function performTokenRefresh(refreshConfig, refreshToken, logger, original
|
|
|
117
117
|
return null;
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* A refresh that DEFINITIVELY failed, remembered just long enough to stop the
|
|
122
|
+
* next twenty requests from asking again.
|
|
123
|
+
*
|
|
124
|
+
* `pendingRefreshes` collapses refreshes that overlap in time; the moment one
|
|
125
|
+
* settles its key is dropped, so a page whose widgets fire in sequence gets a
|
|
126
|
+
* fresh attempt per widget. Measured on a live install with a revoked session:
|
|
127
|
+
* 16 refresh calls in one minute and 51 over ten, every one answered 401, every
|
|
128
|
+
* one a round trip to the auth service for an answer that could not change.
|
|
129
|
+
*
|
|
130
|
+
* Keyed by the same token tail as the dedupe, so a NEW sign-in — a different
|
|
131
|
+
* token — is never suppressed. Short-lived on purpose: this is here to collapse
|
|
132
|
+
* one page's burst, not to cache a verdict. Anything longer would turn a
|
|
133
|
+
* transient blip into a sign-out the user cannot retry their way out of.
|
|
134
|
+
*/ const failedRefreshes = new Map();
|
|
135
|
+
/** How long a definitive failure suppresses another attempt. */ export const REFRESH_FAILURE_MEMORY_MS = 5000;
|
|
136
|
+
/** Exposed for tests; also called when a refresh succeeds. */ export function clearRefreshFailure(dedupeKey) {
|
|
137
|
+
failedRefreshes.delete(dedupeKey);
|
|
138
|
+
}
|
|
139
|
+
function refreshRecentlyFailed(dedupeKey, nowMs) {
|
|
140
|
+
const failedAt = failedRefreshes.get(dedupeKey);
|
|
141
|
+
if (failedAt === undefined) return false;
|
|
142
|
+
if (nowMs - failedAt <= REFRESH_FAILURE_MEMORY_MS) return true;
|
|
143
|
+
failedRefreshes.delete(dedupeKey);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
120
146
|
export function deduplicatedRefresh(refreshConfig, refreshToken, logger, originalCookieHeader) {
|
|
121
147
|
const dedupeKey = refreshToken.slice(-16);
|
|
122
148
|
const existing = pendingRefreshes.get(dedupeKey);
|
|
@@ -124,7 +150,22 @@ export function deduplicatedRefresh(refreshConfig, refreshToken, logger, origina
|
|
|
124
150
|
logger.info('[TokenRefresh] Deduplicating concurrent refresh');
|
|
125
151
|
return existing;
|
|
126
152
|
}
|
|
127
|
-
|
|
153
|
+
if (refreshRecentlyFailed(dedupeKey, Date.now())) {
|
|
154
|
+
// Said once per window rather than per request: the point of this branch is
|
|
155
|
+
// to stop a burst, and a log line per suppressed attempt would reproduce the
|
|
156
|
+
// noise in the log that the requests made on the wire.
|
|
157
|
+
return Promise.resolve(null);
|
|
158
|
+
}
|
|
159
|
+
const promise = performTokenRefresh(refreshConfig, refreshToken, logger, originalCookieHeader).then((result)=>{
|
|
160
|
+
/*
|
|
161
|
+
* `null` is the definitive answer — `performTokenRefresh` returns it when
|
|
162
|
+
* the auth service refused, not when the call blew up (that rejects). So
|
|
163
|
+
* remembering it here suppresses only refusals, and a network fault still
|
|
164
|
+
* gets retried by the next request.
|
|
165
|
+
*/ if (result === null) failedRefreshes.set(dedupeKey, Date.now());
|
|
166
|
+
else clearRefreshFailure(dedupeKey);
|
|
167
|
+
return result;
|
|
168
|
+
}).finally(()=>{
|
|
128
169
|
pendingRefreshes.delete(dedupeKey);
|
|
129
170
|
});
|
|
130
171
|
pendingRefreshes.set(dedupeKey, promise);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|