@nilejs/nile 0.0.6 → 0.0.7

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/index.js CHANGED
@@ -1,3 +1,74 @@
1
+ // auth/jwt-handler.ts
2
+ import { verify } from "hono/jwt";
3
+ import { Err, Ok } from "slang-ts";
4
+ function extractUserId(claims) {
5
+ const value = claims.userId ?? claims.id ?? claims.sub;
6
+ return typeof value === "string" ? value : null;
7
+ }
8
+ function extractOrganizationId(claims) {
9
+ const value = claims.organizationId ?? claims.organization_id ?? claims.orgId;
10
+ return typeof value === "string" ? value : null;
11
+ }
12
+ function extractTokenFromHeader(headers, headerName) {
13
+ if (!headers) {
14
+ return Ok(null);
15
+ }
16
+ const authHeader = headers.get(headerName);
17
+ if (!authHeader) {
18
+ return Ok(null);
19
+ }
20
+ if (!authHeader.startsWith("Bearer ")) {
21
+ return Err("Authorization header must use Bearer scheme");
22
+ }
23
+ return Ok(authHeader.substring(7));
24
+ }
25
+ function extractTokenFromCookie(cookies, cookieName) {
26
+ if (!cookies) {
27
+ return null;
28
+ }
29
+ return cookies[cookieName] ?? null;
30
+ }
31
+ function extractToken(context, config) {
32
+ const method = config.method ?? "header";
33
+ if (method === "header") {
34
+ const headerName = config.headerName ?? "authorization";
35
+ return extractTokenFromHeader(context.headers, headerName);
36
+ }
37
+ const cookieName = config.cookieName ?? "auth_token";
38
+ return Ok(extractTokenFromCookie(context.cookies, cookieName));
39
+ }
40
+ async function verifyJWT(context, config) {
41
+ const tokenResult = extractToken(context, config);
42
+ if (tokenResult.isErr) {
43
+ return Err(tokenResult.error);
44
+ }
45
+ const token = tokenResult.value;
46
+ if (!token) {
47
+ return Err(`No JWT token found in ${config.method ?? "header"}`);
48
+ }
49
+ try {
50
+ const claims = await verify(token, config.secret, "HS256");
51
+ if (!claims) {
52
+ return Err("Invalid JWT token");
53
+ }
54
+ const userId = extractUserId(claims);
55
+ const organizationId = extractOrganizationId(
56
+ claims
57
+ );
58
+ if (!(userId && organizationId)) {
59
+ return Err("Missing userId or organizationId in JWT token");
60
+ }
61
+ return Ok({
62
+ userId,
63
+ organizationId,
64
+ claims
65
+ });
66
+ } catch (error) {
67
+ const message = error instanceof Error ? error.message : "JWT verification failed";
68
+ return Err(`JWT authentication failed: ${message}`);
69
+ }
70
+ }
71
+
1
72
  // engine/create-action.ts
2
73
  function createAction(config) {
3
74
  return config;
@@ -276,14 +347,14 @@ var createLogger = (appName, config) => {
276
347
  import { safeTry as safeTry4 } from "slang-ts";
277
348
 
278
349
  // engine/engine.ts
279
- import { Err as Err3, Ok as Ok3 } from "slang-ts";
350
+ import { Err as Err4, Ok as Ok4 } from "slang-ts";
280
351
 
281
352
  // utils/db/create-model.ts
282
353
  import { count, desc, eq, lt } from "drizzle-orm";
283
- import { Ok, safeTry } from "slang-ts";
354
+ import { Ok as Ok2, safeTry } from "slang-ts";
284
355
 
285
356
  // utils/handle-error.ts
286
- import { Err } from "slang-ts";
357
+ import { Err as Err2 } from "slang-ts";
287
358
  var CALLER_LINE_REGEX = /at\s+(\S+)\s+/;
288
359
  function inferCallerName() {
289
360
  const _err = new Error("capture stack trace");
@@ -315,7 +386,7 @@ function handleError(params) {
315
386
  message: params.message,
316
387
  data: params.data
317
388
  });
318
- return Err(`[${logId}] ${params.message}`);
389
+ return Err2(`[${logId}] ${params.message}`);
319
390
  }
320
391
 
321
392
  // utils/db/create-transaction-variant.ts
@@ -437,7 +508,7 @@ function createModel(table, options) {
437
508
  atFunction: `${name}.create`
438
509
  });
439
510
  }
440
- return Ok(row);
511
+ return Ok2(row);
441
512
  };
442
513
  const update = async ({ id, data, dbx }) => {
443
514
  const parsed = schemas.update.safeParse(data);
@@ -468,7 +539,7 @@ function createModel(table, options) {
468
539
  atFunction: `${name}.update`
469
540
  });
470
541
  }
471
- return Ok(row);
542
+ return Ok2(row);
472
543
  };
473
544
  const createTx = createTransactionVariant(
474
545
  create
@@ -497,7 +568,7 @@ function createModel(table, options) {
497
568
  atFunction: `${name}.findById`
498
569
  });
499
570
  }
500
- return Ok(row);
571
+ return Ok2(row);
501
572
  };
502
573
  const deleteFn = async (id) => {
503
574
  const db = getDb();
@@ -520,7 +591,7 @@ function createModel(table, options) {
520
591
  atFunction: `${name}.delete`
521
592
  });
522
593
  }
523
- return Ok(row);
594
+ return Ok2(row);
524
595
  };
525
596
  const findAll = async () => {
526
597
  const db = getDb();
@@ -539,7 +610,7 @@ function createModel(table, options) {
539
610
  atFunction: `${name}.findAll`
540
611
  });
541
612
  }
542
- return Ok(result.value ?? []);
613
+ return Ok2(result.value ?? []);
543
614
  };
544
615
  const findOffsetPage = async (limit, offset) => {
545
616
  const db = getDb();
@@ -568,7 +639,7 @@ function createModel(table, options) {
568
639
  }
569
640
  const items = itemsResult.value ?? [];
570
641
  const total = countResult.value?.[0]?.total ?? 0;
571
- return Ok({
642
+ return Ok2({
572
643
  items,
573
644
  total,
574
645
  hasMore: offset + items.length < total
@@ -599,7 +670,7 @@ function createModel(table, options) {
599
670
  const items = hasMore ? rows.slice(0, limit) : rows;
600
671
  const lastItem = items.at(-1);
601
672
  const nextCursor = lastItem ? String(lastItem[colName] ?? "") || null : null;
602
- return Ok({
673
+ return Ok2({
603
674
  items,
604
675
  nextCursor,
605
676
  hasMore
@@ -656,7 +727,7 @@ function createDiagnosticsLog(prefix, params) {
656
727
  }
657
728
 
658
729
  // engine/pipeline.ts
659
- import { Err as Err2, Ok as Ok2, safeTry as safeTry2 } from "slang-ts";
730
+ import { Err as Err3, Ok as Ok3, safeTry as safeTry2 } from "slang-ts";
660
731
  import { prettifyError } from "zod";
661
732
  async function runHook(hookDef, hookAction, input, nileContext) {
662
733
  const result = await safeTry2(
@@ -681,7 +752,7 @@ async function processHooks(hooks, initialValue, getAction, nileContext, logTarg
681
752
  log(errorMsg);
682
753
  if (hookDef.isCritical) {
683
754
  nileContext.setHookError(errorMsg);
684
- return Err2(errorMsg);
755
+ return Err3(errorMsg);
685
756
  }
686
757
  continue;
687
758
  }
@@ -700,57 +771,57 @@ async function processHooks(hooks, initialValue, getAction, nileContext, logTarg
700
771
  );
701
772
  if (hookDef.isCritical) {
702
773
  nileContext.setHookError(errorMsg);
703
- return Err2(errorMsg);
774
+ return Err3(errorMsg);
704
775
  }
705
776
  continue;
706
777
  }
707
778
  currentValue = result.value;
708
779
  }
709
- return Ok2(currentValue);
780
+ return Ok3(currentValue);
710
781
  }
711
782
  async function runGlobalBeforeHook(handler, nileContext, action, payload, log) {
712
783
  if (!handler) {
713
- return Ok2(true);
784
+ return Ok3(true);
714
785
  }
715
786
  const result = await safeTry2(() => handler({ nileContext, action, payload }));
716
787
  if (result.isErr) {
717
788
  log(`Global before hook failed for ${action.name}`);
718
789
  nileContext.setHookError(result.error);
719
- return Err2(result.error);
790
+ return Err3(result.error);
720
791
  }
721
- return Ok2(true);
792
+ return Ok3(true);
722
793
  }
723
794
  async function runGlobalAfterHook(handler, nileContext, action, payload, currentResult, log) {
724
795
  if (!handler) {
725
- return Ok2(currentResult);
796
+ return Ok3(currentResult);
726
797
  }
727
798
  const result = await safeTry2(
728
799
  () => handler({
729
800
  nileContext,
730
801
  action,
731
802
  payload,
732
- result: Ok2(currentResult)
803
+ result: Ok3(currentResult)
733
804
  })
734
805
  );
735
806
  if (result.isErr) {
736
807
  log(`Global after hook failed for ${action.name}`);
737
808
  nileContext.setHookError(result.error);
738
- return Err2(result.error);
809
+ return Err3(result.error);
739
810
  }
740
- return Ok2(result.value);
811
+ return Ok3(result.value);
741
812
  }
742
813
  function validatePayload(action, payload, nileContext, log) {
743
814
  if (!action.validation) {
744
- return Ok2(payload);
815
+ return Ok3(payload);
745
816
  }
746
817
  const parseResult = action.validation.safeParse(payload);
747
818
  if (!parseResult.success) {
748
819
  const validationError = prettifyError(parseResult.error);
749
820
  log(`Validation failed for ${action.name}`, validationError);
750
821
  nileContext.setHookError(validationError);
751
- return Err2(`Validation failed: ${validationError}`);
822
+ return Err3(`Validation failed: ${validationError}`);
752
823
  }
753
- return Ok2(parseResult.data);
824
+ return Ok3(parseResult.data);
754
825
  }
755
826
  async function runHandler(action, payload, nileContext, log) {
756
827
  const result = await safeTry2(
@@ -759,13 +830,31 @@ async function runHandler(action, payload, nileContext, log) {
759
830
  if (result.isErr) {
760
831
  log(`Handler failed for ${action.name}`, result.error);
761
832
  nileContext.setHookError(result.error);
762
- return Err2(result.error);
833
+ return Err3(result.error);
763
834
  }
764
835
  nileContext.setHookOutput(result.value);
765
- return Ok2(result.value);
836
+ return Ok3(result.value);
766
837
  }
767
838
 
768
839
  // engine/engine.ts
840
+ async function authenticateAction(action, auth, authContext, nileContext, serviceName, actionName, log) {
841
+ if (!(action.isProtected && auth)) {
842
+ return Ok4(void 0);
843
+ }
844
+ if (!authContext) {
845
+ return Err4("Authentication required: no auth context provided");
846
+ }
847
+ const authResult = await verifyJWT(authContext, auth);
848
+ if (authResult.isErr) {
849
+ log(`Auth failed for ${serviceName}.${actionName}: ${authResult.error}`);
850
+ return Err4(authResult.error);
851
+ }
852
+ nileContext.authResult = authResult.value;
853
+ log(
854
+ `Auth OK for ${serviceName}.${actionName} (user: ${authResult.value.userId})`
855
+ );
856
+ return Ok4(void 0);
857
+ }
769
858
  function createEngine(options) {
770
859
  const { diagnostics, services, logger } = options;
771
860
  const log = createDiagnosticsLog("Engine", {
@@ -776,11 +865,25 @@ function createEngine(options) {
776
865
  const serviceActionsStore = {};
777
866
  const actionStore = {};
778
867
  const initStartTime = performance.now();
868
+ const seenServiceNames = /* @__PURE__ */ new Set();
779
869
  for (const service of services) {
870
+ if (seenServiceNames.has(service.name)) {
871
+ throw new Error(
872
+ `Duplicate service name '${service.name}'. Service names must be unique.`
873
+ );
874
+ }
875
+ seenServiceNames.add(service.name);
876
+ const seenActionNames = /* @__PURE__ */ new Set();
780
877
  const actionNames = [];
781
878
  serviceActionsStore[service.name] = [];
782
879
  actionStore[service.name] = {};
783
880
  for (const action of service.actions) {
881
+ if (seenActionNames.has(action.name)) {
882
+ throw new Error(
883
+ `Duplicate action name '${action.name}' in service '${service.name}'. Action names must be unique within a service.`
884
+ );
885
+ }
886
+ seenActionNames.add(action.name);
784
887
  actionNames.push(action.name);
785
888
  serviceActionsStore[service.name]?.push({
786
889
  name: action.name,
@@ -804,27 +907,39 @@ function createEngine(options) {
804
907
  log(
805
908
  `Initialized in ${performance.now() - initStartTime}ms. Loaded ${services.length} services.`
806
909
  );
807
- const getServices = () => Ok3(serviceSummaries);
910
+ const getServices = () => Ok4(serviceSummaries);
808
911
  const getServiceActions = (serviceName) => {
809
912
  const actions = serviceActionsStore[serviceName];
810
- return actions ? Ok3(actions) : Err3(`Service '${serviceName}' not found`);
913
+ return actions ? Ok4(actions) : Err4(`Service '${serviceName}' not found`);
811
914
  };
812
915
  const getAction = (serviceName, actionName) => {
813
916
  const serviceMap = actionStore[serviceName];
814
917
  if (!serviceMap) {
815
- return Err3(`Service '${serviceName}' not found`);
918
+ return Err4(`Service '${serviceName}' not found`);
816
919
  }
817
920
  const action = serviceMap[actionName];
818
- return action ? Ok3(action) : Err3(`Action '${actionName}' not found in service '${serviceName}'`);
921
+ return action ? Ok4(action) : Err4(`Action '${actionName}' not found in service '${serviceName}'`);
819
922
  };
820
- const executeAction = async (serviceName, actionName, payload, nileContext) => {
923
+ const executeAction = async (serviceName, actionName, payload, nileContext, authContext) => {
821
924
  const { onBeforeActionHandler, onAfterActionHandler } = options;
822
925
  const actionResult = getAction(serviceName, actionName);
823
926
  if (actionResult.isErr) {
824
- return Err3(actionResult.error);
927
+ return Err4(actionResult.error);
825
928
  }
826
929
  const action = actionResult.value;
827
930
  nileContext.resetHookContext(`${serviceName}.${actionName}`, payload);
931
+ const authStep = await authenticateAction(
932
+ action,
933
+ options.auth,
934
+ authContext,
935
+ nileContext,
936
+ serviceName,
937
+ actionName,
938
+ log
939
+ );
940
+ if (authStep.isErr) {
941
+ return Err4(authStep.error);
942
+ }
828
943
  const globalBeforeResult = await runGlobalBeforeHook(
829
944
  onBeforeActionHandler,
830
945
  nileContext,
@@ -833,7 +948,7 @@ function createEngine(options) {
833
948
  log
834
949
  );
835
950
  if (globalBeforeResult.isErr) {
836
- return Err3(globalBeforeResult.error);
951
+ return Err4(globalBeforeResult.error);
837
952
  }
838
953
  const beforeHooksResult = await processHooks(
839
954
  action.hooks?.before ?? [],
@@ -844,7 +959,7 @@ function createEngine(options) {
844
959
  log
845
960
  );
846
961
  if (beforeHooksResult.isErr) {
847
- return Err3(beforeHooksResult.error);
962
+ return Err4(beforeHooksResult.error);
848
963
  }
849
964
  const validationResult = validatePayload(
850
965
  action,
@@ -853,7 +968,7 @@ function createEngine(options) {
853
968
  log
854
969
  );
855
970
  if (validationResult.isErr) {
856
- return Err3(validationResult.error);
971
+ return Err4(validationResult.error);
857
972
  }
858
973
  const handlerResult = await runHandler(
859
974
  action,
@@ -862,7 +977,7 @@ function createEngine(options) {
862
977
  log
863
978
  );
864
979
  if (handlerResult.isErr) {
865
- return Err3(handlerResult.error);
980
+ return Err4(handlerResult.error);
866
981
  }
867
982
  const afterHooksResult = await processHooks(
868
983
  action.hooks?.after ?? [],
@@ -873,7 +988,7 @@ function createEngine(options) {
873
988
  log
874
989
  );
875
990
  if (afterHooksResult.isErr) {
876
- return Err3(afterHooksResult.error);
991
+ return Err4(afterHooksResult.error);
877
992
  }
878
993
  const globalAfterResult = await runGlobalAfterHook(
879
994
  onAfterActionHandler,
@@ -884,12 +999,12 @@ function createEngine(options) {
884
999
  log
885
1000
  );
886
1001
  if (globalAfterResult.isErr) {
887
- return Err3(globalAfterResult.error);
1002
+ return Err4(globalAfterResult.error);
888
1003
  }
889
- return action.result?.pipeline ? Ok3({
1004
+ return action.result?.pipeline ? Ok4({
890
1005
  data: globalAfterResult.value,
891
1006
  pipeline: nileContext.hookContext.log
892
- }) : Ok3(globalAfterResult.value);
1007
+ }) : Ok4(globalAfterResult.value);
893
1008
  };
894
1009
  return {
895
1010
  getServices,
@@ -901,6 +1016,7 @@ function createEngine(options) {
901
1016
 
902
1017
  // rest/rest.ts
903
1018
  import { Hono } from "hono";
1019
+ import { getCookie } from "hono/cookie";
904
1020
  import z2 from "zod";
905
1021
 
906
1022
  // cors/cors.ts
@@ -986,7 +1102,7 @@ var evaluateResolver = (resolver, origin, c, defaultOpts) => {
986
1102
  };
987
1103
 
988
1104
  // rest/intent-handlers.ts
989
- import { Ok as Ok4 } from "slang-ts";
1105
+ import { Ok as Ok5 } from "slang-ts";
990
1106
  import z from "zod";
991
1107
  function toExternalResponse(result, successMessage) {
992
1108
  if (result.isOk) {
@@ -1013,7 +1129,7 @@ function handleExplore(engine, request) {
1013
1129
  }
1014
1130
  const act = actionResult.value;
1015
1131
  return toExternalResponse(
1016
- Ok4({
1132
+ Ok5({
1017
1133
  name: act.name,
1018
1134
  description: act.description,
1019
1135
  isProtected: act.isProtected ?? false,
@@ -1024,7 +1140,7 @@ function handleExplore(engine, request) {
1024
1140
  `Details for '${service}.${action}'`
1025
1141
  );
1026
1142
  }
1027
- async function handleExecute(engine, request, nileContext) {
1143
+ async function handleExecute(engine, request, nileContext, authContext) {
1028
1144
  const { service, action, payload } = request;
1029
1145
  if (service === "*" || action === "*") {
1030
1146
  return {
@@ -1037,7 +1153,8 @@ async function handleExecute(engine, request, nileContext) {
1037
1153
  service,
1038
1154
  action,
1039
1155
  payload,
1040
- nileContext
1156
+ nileContext,
1157
+ authContext
1041
1158
  );
1042
1159
  return toExternalResponse(result, `Action '${service}.${action}' executed`);
1043
1160
  }
@@ -1060,7 +1177,7 @@ function handleSchema(engine, request) {
1060
1177
  actionsResult.value.map((a) => a.name)
1061
1178
  );
1062
1179
  }
1063
- return toExternalResponse(Ok4(schemas), "All service schemas");
1180
+ return toExternalResponse(Ok5(schemas), "All service schemas");
1064
1181
  }
1065
1182
  if (action === "*") {
1066
1183
  const actionsResult = engine.getServiceActions(service);
@@ -1072,7 +1189,7 @@ function handleSchema(engine, request) {
1072
1189
  service,
1073
1190
  actionsResult.value.map((a) => a.name)
1074
1191
  );
1075
- return toExternalResponse(Ok4(schemas), `Schemas for '${service}'`);
1192
+ return toExternalResponse(Ok5(schemas), `Schemas for '${service}'`);
1076
1193
  }
1077
1194
  const actionResult = engine.getAction(service, action);
1078
1195
  if (actionResult.isErr) {
@@ -1080,7 +1197,7 @@ function handleSchema(engine, request) {
1080
1197
  }
1081
1198
  const schema = extractActionSchema(actionResult.value);
1082
1199
  return toExternalResponse(
1083
- Ok4({ [action]: schema }),
1200
+ Ok5({ [action]: schema }),
1084
1201
  `Schema for '${service}.${action}'`
1085
1202
  );
1086
1203
  }
@@ -1114,7 +1231,7 @@ function safeTrySync(fn) {
1114
1231
  }
1115
1232
  var intentHandlers = {
1116
1233
  explore: (engine, request) => handleExplore(engine, request),
1117
- execute: (engine, request, nileContext) => handleExecute(engine, request, nileContext),
1234
+ execute: (engine, request, nileContext, authContext) => handleExecute(engine, request, nileContext, authContext),
1118
1235
  schema: (engine, request) => handleSchema(engine, request)
1119
1236
  };
1120
1237
 
@@ -1152,12 +1269,17 @@ function applyRateLimiting(app, config, log) {
1152
1269
  `Rate limiting enabled: ${rateLimiting.limit ?? DEFAULT_RATE_LIMIT_MAX} requests per ${rateLimiting.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS}ms window`
1153
1270
  );
1154
1271
  }
1272
+ var STATIC_ADAPTER_MODULES = {
1273
+ bun: "hono/bun",
1274
+ node: "@hono/node-server/serve-static"
1275
+ };
1155
1276
  function applyStaticServing(app, config, runtime, log) {
1156
1277
  if (!config.enableStatic) {
1157
1278
  return;
1158
1279
  }
1159
- if (runtime !== "bun") {
1160
- log(`Static file serving not yet supported for runtime: ${runtime}`);
1280
+ const adapterModule = STATIC_ADAPTER_MODULES[runtime];
1281
+ if (!adapterModule) {
1282
+ log(`Static file serving not supported for runtime: ${runtime}`);
1161
1283
  return;
1162
1284
  }
1163
1285
  let cachedHandler = null;
@@ -1168,14 +1290,17 @@ function applyStaticServing(app, config, runtime, log) {
1168
1290
  }
1169
1291
  if (!cachedHandler) {
1170
1292
  const importResult = await safeTry3(async () => {
1171
- const mod = await import("hono/bun");
1293
+ const mod = await import(adapterModule);
1172
1294
  return mod.serveStatic({
1173
1295
  root: "./assets",
1174
1296
  rewriteRequestPath: (path) => path.replace(ASSETS_REGEX, "")
1175
1297
  });
1176
1298
  });
1177
1299
  if (importResult.isErr) {
1178
- log("Failed to load static file adapter", importResult.error);
1300
+ log(
1301
+ `Failed to load static file adapter for ${runtime}`,
1302
+ importResult.error
1303
+ );
1179
1304
  importFailed = true;
1180
1305
  return next();
1181
1306
  }
@@ -1185,7 +1310,255 @@ function applyStaticServing(app, config, runtime, log) {
1185
1310
  return cachedHandler(c, next);
1186
1311
  }
1187
1312
  });
1188
- log("Static file serving enabled at /assets/*");
1313
+ log(`Static file serving enabled at /assets/* (runtime: ${runtime})`);
1314
+ }
1315
+
1316
+ // rest/uploads/validate-files.ts
1317
+ var DEFAULT_MAX_FILES = 10;
1318
+ var DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
1319
+ var DEFAULT_MIN_FILE_SIZE = 1;
1320
+ var DEFAULT_MAX_TOTAL_SIZE = 20 * 1024 * 1024;
1321
+ var DEFAULT_MAX_FILENAME_LENGTH = 128;
1322
+ var DEFAULT_ALLOWED_MIMES = ["image/png", "image/jpeg", "application/pdf"];
1323
+ var DEFAULT_ALLOWED_EXTENSIONS = [".png", ".jpg", ".jpeg", ".pdf"];
1324
+ var PASS = { status: true };
1325
+ function validateFilenameLength(files, maxLength) {
1326
+ const tooLong = files.filter((file) => file.name.length > maxLength);
1327
+ if (tooLong.length === 0) {
1328
+ return PASS;
1329
+ }
1330
+ return {
1331
+ status: false,
1332
+ message: "file name too long",
1333
+ data: {
1334
+ error_category: "validation",
1335
+ files: tooLong.map((f) => f.name),
1336
+ maxLength
1337
+ }
1338
+ };
1339
+ }
1340
+ function validateZeroByteFiles(files) {
1341
+ const emptyFiles = files.filter((file) => file.size === 0);
1342
+ if (emptyFiles.length === 0) {
1343
+ return PASS;
1344
+ }
1345
+ return {
1346
+ status: false,
1347
+ message: "empty file not allowed",
1348
+ data: {
1349
+ error_category: "validation",
1350
+ files: emptyFiles.map((f) => f.name)
1351
+ }
1352
+ };
1353
+ }
1354
+ function validateMinFileSize(files, minFileSize) {
1355
+ const tooSmall = files.filter((file) => file.size < minFileSize);
1356
+ if (tooSmall.length === 0) {
1357
+ return PASS;
1358
+ }
1359
+ return {
1360
+ status: false,
1361
+ message: "file too small",
1362
+ data: {
1363
+ error_category: "validation",
1364
+ limit: "minFileSize",
1365
+ min: minFileSize,
1366
+ files: tooSmall.map((f) => ({ name: f.name, size: f.size }))
1367
+ }
1368
+ };
1369
+ }
1370
+ function validateFileCount(files, maxFiles) {
1371
+ if (files.length <= maxFiles) {
1372
+ return PASS;
1373
+ }
1374
+ return {
1375
+ status: false,
1376
+ message: "upload limit exceeded",
1377
+ data: {
1378
+ error_category: "validation",
1379
+ limit: "maxFiles",
1380
+ max: maxFiles,
1381
+ received: files.length
1382
+ }
1383
+ };
1384
+ }
1385
+ function validateFileSize(files, maxFileSize) {
1386
+ const oversized = files.filter((file) => file.size > maxFileSize);
1387
+ if (oversized.length === 0) {
1388
+ return PASS;
1389
+ }
1390
+ return {
1391
+ status: false,
1392
+ message: "upload limit exceeded",
1393
+ data: {
1394
+ error_category: "validation",
1395
+ limit: "maxFileSize",
1396
+ max: maxFileSize,
1397
+ files: oversized.map((f) => ({ name: f.name, size: f.size }))
1398
+ }
1399
+ };
1400
+ }
1401
+ function validateTotalSize(files, maxTotalSize) {
1402
+ const totalSize = files.reduce((sum, file) => sum + file.size, 0);
1403
+ if (totalSize <= maxTotalSize) {
1404
+ return PASS;
1405
+ }
1406
+ return {
1407
+ status: false,
1408
+ message: "upload limit exceeded",
1409
+ data: {
1410
+ error_category: "validation",
1411
+ limit: "maxTotalSize",
1412
+ max: maxTotalSize,
1413
+ total: totalSize
1414
+ }
1415
+ };
1416
+ }
1417
+ function validateAllowlist(files, allowedMimes, allowedExtensions) {
1418
+ const rejected = files.filter((file) => {
1419
+ const matchesMime = allowedMimes.includes(file.type);
1420
+ const matchesExt = allowedExtensions.some(
1421
+ (ext) => file.name.toLowerCase().endsWith(ext.toLowerCase())
1422
+ );
1423
+ return !(matchesMime && matchesExt);
1424
+ });
1425
+ if (rejected.length === 0) {
1426
+ return PASS;
1427
+ }
1428
+ return {
1429
+ status: false,
1430
+ message: "file type not allowed",
1431
+ data: {
1432
+ error_category: "validation",
1433
+ rejected: rejected.map((f) => ({ name: f.name, type: f.type })),
1434
+ allowed: { mimeTypes: allowedMimes, extensions: allowedExtensions }
1435
+ }
1436
+ };
1437
+ }
1438
+ function validateFiles(files, config) {
1439
+ if (files.length === 0) {
1440
+ return PASS;
1441
+ }
1442
+ const maxFiles = config.limits?.maxFiles ?? DEFAULT_MAX_FILES;
1443
+ const maxFileSize = config.limits?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
1444
+ const minFileSize = config.limits?.minFileSize ?? DEFAULT_MIN_FILE_SIZE;
1445
+ const maxTotalSize = config.limits?.maxTotalSize ?? DEFAULT_MAX_TOTAL_SIZE;
1446
+ const maxFilenameLength = config.limits?.maxFilenameLength ?? DEFAULT_MAX_FILENAME_LENGTH;
1447
+ const allowedMimes = config.allow?.mimeTypes ?? DEFAULT_ALLOWED_MIMES;
1448
+ const allowedExtensions = config.allow?.extensions ?? DEFAULT_ALLOWED_EXTENSIONS;
1449
+ const checks = [
1450
+ validateFilenameLength(files, maxFilenameLength),
1451
+ validateZeroByteFiles(files),
1452
+ validateMinFileSize(files, minFileSize),
1453
+ validateFileCount(files, maxFiles),
1454
+ validateFileSize(files, maxFileSize),
1455
+ validateTotalSize(files, maxTotalSize),
1456
+ validateAllowlist(files, allowedMimes, allowedExtensions)
1457
+ ];
1458
+ for (const check of checks) {
1459
+ if (!check.status) {
1460
+ return check;
1461
+ }
1462
+ }
1463
+ return PASS;
1464
+ }
1465
+
1466
+ // rest/uploads/parse-formdata.ts
1467
+ async function parseBodyToStructured(c) {
1468
+ try {
1469
+ const body = await c.req.parseBody({ all: true });
1470
+ const fields = {};
1471
+ const files = {};
1472
+ const conflicts = [];
1473
+ for (const [key, value] of Object.entries(body)) {
1474
+ if (key === "action") {
1475
+ continue;
1476
+ }
1477
+ if (Array.isArray(value)) {
1478
+ const hasFiles = value.some((v) => v instanceof File);
1479
+ const hasStrings = value.some((v) => typeof v === "string");
1480
+ if (hasFiles && hasStrings) {
1481
+ conflicts.push(key);
1482
+ continue;
1483
+ }
1484
+ if (hasFiles) {
1485
+ files[key] = value.filter((v) => v instanceof File);
1486
+ } else {
1487
+ fields[key] = value.map((v) => String(v));
1488
+ }
1489
+ } else if (value instanceof File) {
1490
+ files[key] = value;
1491
+ } else {
1492
+ fields[key] = String(value);
1493
+ }
1494
+ }
1495
+ if (conflicts.length > 0) {
1496
+ return {
1497
+ status: false,
1498
+ message: "mixed key types not allowed",
1499
+ errorData: {
1500
+ error_category: "validation",
1501
+ conflicts,
1502
+ hint: "Same key cannot be used for both files and fields"
1503
+ }
1504
+ };
1505
+ }
1506
+ return { status: true, data: { fields, files } };
1507
+ } catch (error) {
1508
+ return {
1509
+ status: false,
1510
+ message: "failed to parse request body",
1511
+ errorData: {
1512
+ error_category: "parsing",
1513
+ error: error instanceof Error ? error.message : String(error)
1514
+ }
1515
+ };
1516
+ }
1517
+ }
1518
+ function enforceActionContentType(action, contentType, enforceContentType) {
1519
+ if (!(enforceContentType && action.isSpecial?.contentType)) {
1520
+ return { status: true };
1521
+ }
1522
+ const expected = action.isSpecial.contentType;
1523
+ const matches = contentType.toLowerCase().includes(expected.toLowerCase());
1524
+ if (!matches) {
1525
+ return {
1526
+ status: false,
1527
+ statusCode: 415,
1528
+ message: "unsupported content type",
1529
+ data: {
1530
+ error_category: "validation",
1531
+ expected,
1532
+ received: contentType
1533
+ }
1534
+ };
1535
+ }
1536
+ return { status: true };
1537
+ }
1538
+ async function handleFormDataRequest(c, config, _uploadMode = "flat") {
1539
+ const parseResult = await parseBodyToStructured(c);
1540
+ if (!(parseResult.status && parseResult.data)) {
1541
+ return parseResult;
1542
+ }
1543
+ const payload = parseResult.data;
1544
+ const allFiles = [];
1545
+ for (const value of Object.values(payload.files)) {
1546
+ if (Array.isArray(value)) {
1547
+ allFiles.push(...value);
1548
+ } else {
1549
+ allFiles.push(value);
1550
+ }
1551
+ }
1552
+ const validationResult = validateFiles(allFiles, config);
1553
+ if (!validationResult.status) {
1554
+ return {
1555
+ status: false,
1556
+ message: validationResult.message,
1557
+ errorData: validationResult.data,
1558
+ statusCode: validationResult.statusCode
1559
+ };
1560
+ }
1561
+ return { status: true, data: payload };
1189
1562
  }
1190
1563
 
1191
1564
  // rest/rest.ts
@@ -1195,6 +1568,87 @@ var externalRequestSchema = z2.object({
1195
1568
  action: z2.string().min(1),
1196
1569
  payload: z2.record(z2.string(), z2.unknown())
1197
1570
  });
1571
+ var formDataRoutingSchema = z2.object({
1572
+ intent: z2.enum(["explore", "execute", "schema"]),
1573
+ service: z2.string().min(1),
1574
+ action: z2.string().min(1)
1575
+ });
1576
+ async function handleFormDataPath(c, config, engine, nileContext, authContext, log) {
1577
+ const rawBody = await c.req.parseBody({ all: true }).catch(() => null);
1578
+ if (!rawBody) {
1579
+ return c.json(
1580
+ {
1581
+ status: false,
1582
+ message: "Failed to parse multipart form data",
1583
+ data: {}
1584
+ },
1585
+ 400
1586
+ );
1587
+ }
1588
+ const routing = formDataRoutingSchema.safeParse({
1589
+ intent: rawBody.intent,
1590
+ service: rawBody.service,
1591
+ action: rawBody.action
1592
+ });
1593
+ if (!routing.success) {
1594
+ return c.json(
1595
+ {
1596
+ status: false,
1597
+ message: "Form-data must include 'intent', 'service', and 'action' fields",
1598
+ data: { errors: routing.error.issues }
1599
+ },
1600
+ 400
1601
+ );
1602
+ }
1603
+ const { intent, service, action } = routing.data;
1604
+ log(`${intent} -> ${service}.${action} (form-data)`);
1605
+ const actionResult = engine.getAction(service, action);
1606
+ if (actionResult.isOk && config.uploads?.enforceContentType) {
1607
+ const contentTypeCheck = enforceActionContentType(
1608
+ actionResult.value,
1609
+ "multipart/form-data",
1610
+ true
1611
+ );
1612
+ if (!contentTypeCheck.status) {
1613
+ return c.json(
1614
+ {
1615
+ status: false,
1616
+ message: contentTypeCheck.message ?? "Unsupported content type",
1617
+ data: contentTypeCheck.data ?? {}
1618
+ },
1619
+ contentTypeCheck.statusCode ?? 415
1620
+ );
1621
+ }
1622
+ }
1623
+ const uploadConfig = config.uploads ?? {};
1624
+ const uploadMode = actionResult.isOk ? actionResult.value.isSpecial?.uploadMode ?? "flat" : "flat";
1625
+ const uploadResult = await handleFormDataRequest(c, uploadConfig, uploadMode);
1626
+ if (!(uploadResult.status && uploadResult.data)) {
1627
+ return c.json(
1628
+ {
1629
+ status: false,
1630
+ message: uploadResult.message ?? "Upload validation failed",
1631
+ data: uploadResult.errorData ?? {}
1632
+ },
1633
+ uploadResult.statusCode ?? 400
1634
+ );
1635
+ }
1636
+ const request = {
1637
+ intent,
1638
+ service,
1639
+ action,
1640
+ payload: uploadResult.data
1641
+ };
1642
+ const handler = intentHandlers[request.intent];
1643
+ const response = await handler(
1644
+ engine,
1645
+ request,
1646
+ nileContext,
1647
+ authContext
1648
+ );
1649
+ const statusCode = response.status ? 200 : 400;
1650
+ return c.json(response, statusCode);
1651
+ }
1198
1652
  function createRestApp(params) {
1199
1653
  const { config, engine, nileContext, serverName, runtime } = params;
1200
1654
  const app = new Hono();
@@ -1207,6 +1661,22 @@ function createRestApp(params) {
1207
1661
  applyStaticServing(app, config, runtime, log);
1208
1662
  const servicesPath = `${config.baseUrl}/services`;
1209
1663
  app.post(servicesPath, async (c) => {
1664
+ const contentType = c.req.header("content-type") ?? "";
1665
+ const isFormData = contentType.includes("multipart/form-data");
1666
+ const authContext = {
1667
+ headers: c.req.raw.headers,
1668
+ cookies: getCookie(c)
1669
+ };
1670
+ if (isFormData) {
1671
+ return handleFormDataPath(
1672
+ c,
1673
+ config,
1674
+ engine,
1675
+ nileContext,
1676
+ authContext,
1677
+ log
1678
+ );
1679
+ }
1210
1680
  const body = await c.req.json().catch(() => null);
1211
1681
  if (!body) {
1212
1682
  return c.json(
@@ -1232,7 +1702,12 @@ function createRestApp(params) {
1232
1702
  const request = parsed.data;
1233
1703
  log(`${request.intent} -> ${request.service}.${request.action}`);
1234
1704
  const handler = intentHandlers[request.intent];
1235
- const response = await handler(engine, request, nileContext);
1705
+ const response = await handler(
1706
+ engine,
1707
+ request,
1708
+ nileContext,
1709
+ authContext
1710
+ );
1236
1711
  const statusCode = response.status ? 200 : 400;
1237
1712
  return c.json(response, statusCode);
1238
1713
  });
@@ -1283,6 +1758,20 @@ function createNileContext(params) {
1283
1758
  setSession(name, data) {
1284
1759
  sessions[name] = data;
1285
1760
  },
1761
+ authResult: void 0,
1762
+ getAuth() {
1763
+ return context.authResult;
1764
+ },
1765
+ getUser() {
1766
+ if (!context.authResult) {
1767
+ return void 0;
1768
+ }
1769
+ return {
1770
+ userId: context.authResult.userId,
1771
+ organizationId: context.authResult.organizationId,
1772
+ ...context.authResult.claims
1773
+ };
1774
+ },
1286
1775
  hookContext: {
1287
1776
  actionName: "",
1288
1777
  input: null,
@@ -1341,6 +1830,7 @@ function createNileServer(config) {
1341
1830
  services: config.services,
1342
1831
  diagnostics: config.diagnostics,
1343
1832
  logger: config.resources?.logger,
1833
+ auth: config.auth,
1344
1834
  onBeforeActionHandler: config.onBeforeActionHandler,
1345
1835
  onAfterActionHandler: config.onAfterActionHandler
1346
1836
  });
@@ -1406,6 +1896,9 @@ export {
1406
1896
  getContext,
1407
1897
  getLogs,
1408
1898
  getZodSchema,
1409
- handleError
1899
+ handleError,
1900
+ handleFormDataRequest,
1901
+ validateFiles,
1902
+ verifyJWT
1410
1903
  };
1411
1904
  //# sourceMappingURL=index.js.map