@archlast/server 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +214 -56
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -407,12 +407,12 @@ export const processImage = action({
407
407
  Explicit HTTP endpoints with full control over request/response.
408
408
 
409
409
  ```ts
410
- import { http } from "@archlast/server/functions/definition";
410
+ import { http } from "@archlast/server/http/definition";
411
411
 
412
412
  // GET route
413
413
  export const health = http.get({
414
414
  path: "/health",
415
- handler: async () => {
415
+ handler: async (ctx) => {
416
416
  return new Response("ok", { status: 200 });
417
417
  },
418
418
  });
@@ -420,8 +420,8 @@ export const health = http.get({
420
420
  // POST route with body parsing
421
421
  export const createWebhook = http.post({
422
422
  path: "/api/webhooks",
423
- handler: async (ctx, request) => {
424
- const body = await request.json();
423
+ handler: async (ctx) => {
424
+ const body = await ctx.req.json();
425
425
  // Process webhook...
426
426
  return Response.json({ received: true });
427
427
  },
@@ -430,8 +430,9 @@ export const createWebhook = http.post({
430
430
  // Route with path parameters
431
431
  export const getUser = http.get({
432
432
  path: "/api/users/:id",
433
- handler: async (ctx, request, params) => {
434
- const user = await ctx.db.get("users", params.id);
433
+ handler: async (ctx) => {
434
+ const userId = ctx.req.params.id;
435
+ const user = await ctx.server.db.get("users", userId);
435
436
  if (!user) {
436
437
  return new Response("Not found", { status: 404 });
437
438
  }
@@ -440,41 +441,76 @@ export const getUser = http.get({
440
441
  });
441
442
 
442
443
  // Other methods
443
- http.put({ path: "/api/resource/:id", handler: async (ctx, req) => { /* ... */ } });
444
- http.patch({ path: "/api/resource/:id", handler: async (ctx, req) => { /* ... */ } });
445
- http.delete({ path: "/api/resource/:id", handler: async (ctx, req) => { /* ... */ } });
446
- http.options({ path: "/api/resource", handler: async () => { /* ... */ } });
444
+ export const updateResource = http.put({
445
+ path: "/api/resource/:id",
446
+ handler: async (ctx) => {
447
+ const id = ctx.req.params.id;
448
+ const data = await ctx.req.json();
449
+ // Update resource...
450
+ return Response.json({ success: true });
451
+ },
452
+ });
453
+
454
+ export const patchResource = http.patch({
455
+ path: "/api/resource/:id",
456
+ handler: async (ctx) => { /* ... */ return Response.json({ success: true }); },
457
+ });
458
+
459
+ export const deleteResource = http.delete({
460
+ path: "/api/resource/:id",
461
+ handler: async (ctx) => { /* ... */ return new Response(null, { status: 204 }); },
462
+ });
463
+
464
+ export const optionsResource = http.options({
465
+ path: "/api/resource",
466
+ handler: async (ctx) => { return new Response(null, { status: 204 }); },
467
+ });
447
468
  ```
448
469
 
449
470
  ### Webhooks
450
471
 
451
- Signed webhook handlers with automatic verification.
472
+ Webhook handlers with custom security guards.
452
473
 
453
474
  ```ts
454
- import { webhook } from "@archlast/server/functions/definition";
475
+ import { webhook } from "@archlast/server/webhook/definition";
476
+ import { createSignatureGuard } from "@archlast/server/webhook/guard";
455
477
 
478
+ // Stripe webhook with signature verification
456
479
  export const stripeWebhook = webhook({
457
480
  path: "/webhooks/stripe",
458
- secret: process.env.STRIPE_WEBHOOK_SECRET!,
481
+ method: "POST",
482
+ guards: [
483
+ createSignatureGuard({
484
+ secret: process.env.STRIPE_WEBHOOK_SECRET!,
485
+ header: "stripe-signature",
486
+ algorithm: "sha256",
487
+ }),
488
+ ],
459
489
  handler: async (ctx, event) => {
460
490
  switch (event.type) {
461
491
  case "checkout.session.completed":
462
- await handleCheckoutComplete(ctx, event.data);
492
+ await ctx.db.insert("orders", { userId: event.data.customer });
463
493
  break;
464
494
  case "customer.subscription.deleted":
465
- await handleSubscriptionCanceled(ctx, event.data);
495
+ await ctx.db.update("users", event.data.customer, { subscribed: false });
466
496
  break;
467
497
  }
468
498
  return { received: true };
469
499
  },
470
500
  });
471
501
 
502
+ // GitHub webhook with signature verification
472
503
  export const githubWebhook = webhook({
473
504
  path: "/webhooks/github",
474
- secret: process.env.GITHUB_WEBHOOK_SECRET!,
475
- signatureHeader: "X-Hub-Signature-256",
505
+ guards: [
506
+ createSignatureGuard({
507
+ secret: process.env.GITHUB_WEBHOOK_SECRET!,
508
+ header: "X-Hub-Signature-256",
509
+ algorithm: "sha256",
510
+ }),
511
+ ],
476
512
  handler: async (ctx, payload) => {
477
- console.log("GitHub event:", payload.action);
513
+ ctx.logger.info("GitHub event received", { action: payload.action });
478
514
  return { ok: true };
479
515
  },
480
516
  });
@@ -486,22 +522,29 @@ Type-safe RPC procedures with automatic router generation.
486
522
 
487
523
  ```ts
488
524
  import { rpc } from "@archlast/server/functions/definition";
525
+ import { z } from "zod";
489
526
 
490
527
  // RPC query
491
528
  export const getTasks = rpc.query({
492
- args: { completed: v.boolean().optional().zodSchema },
529
+ args: z.object({
530
+ completed: z.boolean().optional(),
531
+ }),
493
532
  handler: async (ctx, args) => {
494
- return ctx.db.query("tasks")
495
- .where(args.completed !== undefined ? { completed: args.completed } : {})
496
- .findMany();
533
+ const query = ctx.db.query("tasks");
534
+ if (args.completed !== undefined) {
535
+ query.where({ completed: args.completed });
536
+ }
537
+ return query.findMany();
497
538
  },
498
539
  });
499
540
 
500
541
  // RPC mutation
501
542
  export const createTask = rpc.mutation({
502
- args: { text: v.string().zodSchema },
543
+ args: z.object({
544
+ text: z.string(),
545
+ }),
503
546
  handler: async (ctx, args) => {
504
- return ctx.db.insert("tasks", { text: args.text });
547
+ return ctx.db.insert("tasks", { text: args.text, completed: false });
505
548
  },
506
549
  });
507
550
  ```
@@ -542,15 +585,49 @@ interface ActionCtx extends MutationCtx {
542
585
 
543
586
  ### AuthContext
544
587
 
588
+ The auth context is populated from Better-Auth session data:
589
+
545
590
  ```ts
546
591
  interface AuthContext {
592
+ // User ID from Better-Auth session
547
593
  userId: string | null;
594
+
595
+ // Session ID from Better-Auth
548
596
  sessionId: string | null;
597
+
598
+ // Whether the request is authenticated
549
599
  isAuthenticated: boolean;
550
- user: User | null;
600
+
601
+ // Better-Auth user object
602
+ user: {
603
+ id: string;
604
+ email: string;
605
+ emailVerified: boolean;
606
+ name?: string;
607
+ image?: string;
608
+ role?: string; // "user", "admin", "super-admin"
609
+ banned?: boolean;
610
+ createdAt: Date;
611
+ updatedAt: Date;
612
+ } | null;
613
+
614
+ // Tenant/Organization info (if using organizations)
615
+ tenant?: {
616
+ id: string;
617
+ name: string;
618
+ ownerId: string;
619
+ } | null;
620
+
621
+ // Membership info
622
+ membership?: {
623
+ role: string;
624
+ permissions: string[];
625
+ } | null;
551
626
  }
552
627
  ```
553
628
 
629
+ **Note:** Better-Auth stores users in `system_auth_user` collection with the `id` field (not `_id`). The adapter handles mapping automatically.
630
+
554
631
  ### Usage Examples
555
632
 
556
633
  ```ts
@@ -596,15 +673,49 @@ export const myAction = action(async (ctx) => {
596
673
 
597
674
  ## Authentication & Permissions
598
675
 
676
+ Archlast uses [Better-Auth](https://www.better-auth.com/) for authentication. The server mounts Better-Auth at `/api/auth/*` and includes these plugins:
677
+
678
+ - **Email/Password**: Standard credential authentication via `signIn.email()`
679
+ - **Username**: Username-based sign-in via `signIn.username()`
680
+ - **Anonymous**: Guest user support via `signIn.anonymous()`
681
+ - **Admin**: User management, roles, bans, impersonation
682
+ - **API Key**: Programmatic access with `arch_` prefixed keys
683
+ - **Organization**: Multi-tenancy support
684
+
685
+ ### Sign-In Methods
686
+
687
+ ```ts
688
+ // Email-based sign-in (web app pattern)
689
+ await authClient.signIn.email({ email: "user@example.com", password: "..." });
690
+
691
+ // Username-based sign-in (dashboard pattern)
692
+ await authClient.signIn.username({ username: "admin", password: "..." });
693
+
694
+ // Anonymous/guest sign-in
695
+ await authClient.signIn.anonymous();
696
+ ```
697
+
698
+ ### Better-Auth Configuration
699
+
700
+ Better-Auth is configured in the server with these features:
701
+
702
+ ```ts
703
+ // Environment variables for Better-Auth
704
+ BETTER_AUTH_SECRET=your-32-char-secret // Required in production
705
+ APP_URL=https://your-app.com // Base URL for redirects
706
+ ARCHLAST_ALLOWED_ORIGINS=http://localhost:3000 // Trusted origins (CSV)
707
+ ```
708
+
599
709
  ### Auth Modes
600
710
 
601
711
  All functions default to requiring authentication. Override with `auth` option:
602
712
 
603
713
  ```ts
604
- // Required (default) - must be authenticated
714
+ // Required (default) - must be authenticated via Better-Auth
605
715
  export const privateQuery = query({
606
716
  handler: async (ctx) => {
607
717
  // ctx.auth.userId is guaranteed to exist
718
+ // ctx.auth.user contains Better-Auth user object
608
719
  },
609
720
  });
610
721
 
@@ -613,9 +724,9 @@ export const optionalAuth = query({
613
724
  auth: "optional",
614
725
  handler: async (ctx) => {
615
726
  if (ctx.auth.isAuthenticated) {
616
- // Authenticated user
727
+ // Authenticated user via Better-Auth
617
728
  } else {
618
- // Anonymous user
729
+ // Anonymous/guest user
619
730
  }
620
731
  },
621
732
  });
@@ -666,13 +777,20 @@ export const scheduleCleanup = mutation(async (ctx) => {
666
777
  payload: { olderThanDays: 30 },
667
778
  });
668
779
 
669
- // Run in 5 minutes
780
+ // Run every 5 minutes
670
781
  await ctx.scheduler.schedule({
671
782
  name: "sendReminder",
672
- preset: SchedulePreset.In5Minutes,
783
+ preset: SchedulePreset.Every5Minutes,
673
784
  payload: { userId: ctx.auth.userId },
674
785
  });
675
786
 
787
+ // Run every 10 minutes
788
+ await ctx.scheduler.schedule({
789
+ name: "syncData",
790
+ preset: SchedulePreset.Every10Minutes,
791
+ payload: {},
792
+ });
793
+
676
794
  // Recurring with cron
677
795
  await ctx.scheduler.schedule({
678
796
  name: "dailyReport",
@@ -684,23 +802,30 @@ export const scheduleCleanup = mutation(async (ctx) => {
684
802
 
685
803
  ### Schedule Presets
686
804
 
687
- | Preset | Description |
688
- |--------|-------------|
689
- | `RunNow` | Execute immediately |
690
- | `EveryMinute` | Run every minute |
691
- | `Every5Minutes` | Run every 5 minutes |
692
- | `Every15Minutes` | Run every 15 minutes |
693
- | `Every30Minutes` | Run every 30 minutes |
694
- | `Hourly` | Run every hour |
695
- | `Every6Hours` | Run every 6 hours |
696
- | `Every12Hours` | Run every 12 hours |
697
- | `DailyMidnight` | Daily at midnight UTC |
698
- | `DailyNoon` | Daily at noon UTC |
699
- | `Weekly` | Weekly on Sunday midnight |
700
- | `Monthly` | Monthly on the 1st |
701
- | `In5Minutes` | Once, 5 minutes from now |
702
- | `In1Hour` | Once, 1 hour from now |
703
- | `In1Day` | Once, 24 hours from now |
805
+ | Preset | Cron Expression | Description |
806
+ |--------|-----------------|-------------|
807
+ | `RunNow` | (immediate) | Execute immediately |
808
+ | `EveryMinute` | `* * * * *` | Run every minute |
809
+ | `Every5Minutes` | `*/5 * * * *` | Run every 5 minutes |
810
+ | `Every10Minutes` | `*/10 * * * *` | Run every 10 minutes |
811
+ | `Every30Minutes` | `*/30 * * * *` | Run every 30 minutes |
812
+ | `Hourly` | `0 * * * *` | Run every hour |
813
+ | `Every8Hours` | `0 */8 * * *` | Run every 8 hours |
814
+ | `Every16Hours` | `0 */16 * * *` | Run every 16 hours |
815
+ | `DailyMidnight` | `0 0 * * *` | Daily at midnight UTC |
816
+ | `Weekly` | `0 0 * * 0` | Weekly on Sunday midnight |
817
+ | `Monthly` | `0 0 1 * *` | Monthly on the 1st |
818
+ | `Quarterly` | `0 0 1 */3 *` | Quarterly (every 3 months) |
819
+
820
+ You can also use custom cron expressions:
821
+
822
+ ```ts
823
+ await ctx.scheduler.schedule({
824
+ name: "customJob",
825
+ cron: "0 9 * * 1-5", // Weekdays at 9 AM
826
+ payload: {},
827
+ });
828
+ ```
704
829
 
705
830
  ---
706
831
 
@@ -740,14 +865,28 @@ Import only what you need:
740
865
  |--------|-------------|
741
866
  | `@archlast/server/schema/definition` | `defineSchema`, `defineTable`, relationship helpers |
742
867
  | `@archlast/server/schema/validators` | `v` validator object |
743
- | `@archlast/server/functions/definition` | `query`, `mutation`, `action`, `http`, `webhook`, `rpc` |
868
+ | `@archlast/server/functions/definition` | `query`, `mutation`, `action`, `rpc` |
744
869
  | `@archlast/server/functions/types` | Context types, function types |
745
- | `@archlast/server/http` | HTTP handler utilities |
746
- | `@archlast/server/webhook` | Webhook handler utilities |
747
- | `@archlast/server/jobs` | `SchedulePreset`, job types |
870
+ | `@archlast/server/http/definition` | `http` route builder |
871
+ | `@archlast/server/http/router` | HTTP router utilities |
872
+ | `@archlast/server/http` | HTTP utilities |
873
+ | `@archlast/server/webhook/definition` | `webhook` function builder |
874
+ | `@archlast/server/webhook/guard` | Webhook guard utilities |
875
+ | `@archlast/server/webhook/verifier` | Signature verification |
876
+ | `@archlast/server/webhook` | Webhook utilities |
877
+ | `@archlast/server/jobs` | `SchedulePreset`, `Scheduler`, `JobQueue` |
878
+ | `@archlast/server/jobs/scheduler` | Scheduler class and presets |
879
+ | `@archlast/server/jobs/queue` | Job queue implementation |
748
880
  | `@archlast/server/storage/types` | Storage type definitions |
749
- | `@archlast/server/context` | Context type exports |
750
- | `@archlast/server/di/*` | Dependency injection utilities |
881
+ | `@archlast/server/context` | Server context exports |
882
+ | `@archlast/server/db/interfaces` | Database interface types |
883
+ | `@archlast/server/auth/interfaces` | Auth interface types |
884
+ | `@archlast/server/repository/interfaces` | Repository interface types |
885
+ | `@archlast/server/repository/factory` | Repository factory |
886
+ | `@archlast/server/di/decorators` | DI decorators |
887
+ | `@archlast/server/di/container` | DI container |
888
+ | `@archlast/server/logging/logger` | Logger service |
889
+ | `@archlast/server/docker` | Docker utilities |
751
890
 
752
891
  ---
753
892
 
@@ -755,6 +894,16 @@ Import only what you need:
755
894
 
756
895
  Key variables used by the server runtime:
757
896
 
897
+ ### Better-Auth Configuration
898
+
899
+ | Variable | Default | Description |
900
+ |----------|---------|-------------|
901
+ | `BETTER_AUTH_SECRET` | - | **Required in production.** Secret for signing tokens |
902
+ | `APP_URL` | `http://localhost:4000` | Base URL for auth redirects |
903
+ | `BETTER_AUTH_DEBUG` | `false` | Enable debug logging |
904
+
905
+ ### Server Configuration
906
+
758
907
  | Variable | Default | Description |
759
908
  |----------|---------|-------------|
760
909
  | `PORT` | `4000` | HTTP server port |
@@ -762,6 +911,11 @@ Key variables used by the server runtime:
762
911
  | `ARCHLAST_DB_ROOT` | `./data` | Database file directory |
763
912
  | `ARCHLAST_ALLOWED_ORIGINS` | - | CORS allowed origins (CSV) |
764
913
  | `ARCHLAST_CORS_ALLOW_CREDENTIALS` | `false` | Allow credentials in CORS |
914
+
915
+ ### Storage Configuration
916
+
917
+ | Variable | Default | Description |
918
+ |----------|---------|-------------|
765
919
  | `STORAGE_ROOT` | `./storage` | Local file storage directory |
766
920
  | `STORAGE_SIGNING_SECRET` | - | Secret for signed URLs |
767
921
  | `S3_ENABLED` | `false` | Enable S3 storage |
@@ -769,7 +923,11 @@ Key variables used by the server runtime:
769
923
  | `S3_REGION` | `us-east-1` | S3 region |
770
924
  | `AWS_ACCESS_KEY_ID` | - | AWS access key |
771
925
  | `AWS_SECRET_ACCESS_KEY` | - | AWS secret key |
772
- | `ARCHLAST_AUTH_TOKEN_PEPPER` | - | Token signing pepper |
926
+
927
+ ### Dashboard & Store Configuration
928
+
929
+ | Variable | Default | Description |
930
+ |----------|---------|-------------|
773
931
  | `ARCHLAST_DASHBOARD_DIR` | - | Dashboard static files path |
774
932
  | `ARCHLAST_DASHBOARD_URL` | - | Dashboard proxy URL |
775
933
  | `ARCHLAST_STORE_PORT` | `7001` | Document store port |
@@ -792,9 +950,9 @@ The CLI uses these templates when running `archlast start`.
792
950
 
793
951
  ---
794
952
 
795
- ## Publishing (Maintainers)
953
+ ## Keywords
796
954
 
797
- See `docs/npm-publishing.md` for release and publish steps.
955
+ archlast, server, backend, reactive, real-time, websocket, typescript, api, baas
798
956
 
799
957
  ## License
800
958
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archlast/server",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Archlast server library exports and Docker templates",
5
5
  "license": "MIT",
6
6
  "repository": {