@checkstack/backend 0.4.4 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @checkstack/backend
2
2
 
3
+ ## 0.4.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 8a87cd4: Added startup validation for unregistered access rules
8
+
9
+ The backend now throws an error at startup if a procedure contract references an access rule that isn't registered with the plugin system. This prevents silent runtime failures.
10
+
11
+ - Updated dependencies [8a87cd4]
12
+ - Updated dependencies [8a87cd4]
13
+ - Updated dependencies [8a87cd4]
14
+ - @checkstack/auth-common@0.5.2
15
+ - @checkstack/backend-api@0.4.1
16
+ - @checkstack/common@0.5.0
17
+ - @checkstack/queue-api@0.1.3
18
+ - @checkstack/signal-backend@0.1.5
19
+ - @checkstack/api-docs-common@0.1.3
20
+ - @checkstack/signal-common@0.1.3
21
+
3
22
  ## 0.4.4
4
23
 
5
24
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/backend",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "bun --env-file=../../.env --watch src/index.ts",
@@ -424,6 +424,35 @@ export async function loadPlugins({
424
424
  }
425
425
  }
426
426
 
427
+ // Phase 2.5: Validate that all access rules used in contracts are registered
428
+ // This catches bugs where access rules are used in procedures but not added to
429
+ // the plugin's accessRules registration array.
430
+ rootLogger.debug("🔍 Validating access rules in contracts...");
431
+ const registeredRuleIds = new Set(
432
+ deps.registeredAccessRules.map((r) => r.id),
433
+ );
434
+ const validationErrors: string[] = [];
435
+
436
+ for (const [pluginId, contract] of deps.pluginContractRegistry) {
437
+ validateContractAccessRules({
438
+ pluginId,
439
+ contract,
440
+ registeredRuleIds,
441
+ validationErrors,
442
+ });
443
+ }
444
+
445
+ if (validationErrors.length > 0) {
446
+ rootLogger.error("❌ Unregistered access rules found in contracts:");
447
+ for (const error of validationErrors) {
448
+ rootLogger.error(` • ${error}`);
449
+ }
450
+ throw new Error(
451
+ `Unregistered access rules in contracts:\n${validationErrors.join("\n")}`,
452
+ );
453
+ }
454
+ rootLogger.debug("✅ All access rules in contracts are registered");
455
+
427
456
  // Phase 3: Run afterPluginsReady callbacks
428
457
  rootLogger.debug("🔄 Running afterPluginsReady callbacks...");
429
458
 
@@ -507,3 +536,42 @@ export async function loadPlugins({
507
536
  }
508
537
  rootLogger.debug("✅ All afterPluginsReady callbacks complete");
509
538
  }
539
+
540
+ /**
541
+ * Validate that all access rules used in a contract are registered with the plugin system.
542
+ * Recursively traverses the contract to find all procedures and their access metadata.
543
+ */
544
+ function validateContractAccessRules({
545
+ pluginId,
546
+ contract,
547
+ registeredRuleIds,
548
+ validationErrors,
549
+ }: {
550
+ pluginId: string;
551
+ contract: AnyContractRouter;
552
+ registeredRuleIds: Set<string>;
553
+ validationErrors: string[];
554
+ }): void {
555
+ for (const [procedureName, procedure] of Object.entries(
556
+ contract as Record<string, unknown>,
557
+ )) {
558
+ if (!procedure || typeof procedure !== "object") continue;
559
+
560
+ // Check if this is a procedure with oRPC metadata
561
+ const orpcData = (procedure as Record<string, unknown>)["~orpc"] as
562
+ | { meta?: { access?: Array<{ id: string }> } }
563
+ | undefined;
564
+
565
+ if (orpcData?.meta?.access) {
566
+ for (const accessRule of orpcData.meta.access) {
567
+ const qualifiedId = `${pluginId}.${accessRule.id}`;
568
+ if (!registeredRuleIds.has(qualifiedId)) {
569
+ validationErrors.push(
570
+ `Plugin "${pluginId}" procedure "${procedureName}" uses unregistered access rule "${accessRule.id}" (qualified: "${qualifiedId}"). ` +
571
+ `Add it to the plugin's accessRules registration array.`,
572
+ );
573
+ }
574
+ }
575
+ }
576
+ }
577
+ }