@lucern/sdk 0.3.0-alpha.12 → 0.3.0-alpha.13

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 (102) hide show
  1. package/README.md +51 -0
  2. package/dist/accessControl.d.ts +1 -0
  3. package/dist/accessControl.js +156 -22
  4. package/dist/accessControl.js.map +1 -1
  5. package/dist/adminClient.js.map +1 -1
  6. package/dist/answersClient.js.map +1 -1
  7. package/dist/audiencesClient.js.map +1 -1
  8. package/dist/auditClient.js.map +1 -1
  9. package/dist/authContext.d.ts +1 -1
  10. package/dist/authContext.js.map +1 -1
  11. package/dist/beliefs/index.d.ts +1 -0
  12. package/dist/beliefs/index.js +206 -40
  13. package/dist/beliefs/index.js.map +1 -1
  14. package/dist/beliefsClient.js.map +1 -1
  15. package/dist/client.d.ts +79 -31
  16. package/dist/client.js +206 -40
  17. package/dist/client.js.map +1 -1
  18. package/dist/contextClient.js.map +1 -1
  19. package/dist/contracts/auth-session.contract.d.ts +1 -1
  20. package/dist/contracts/auth-session.contract.js +13 -1
  21. package/dist/contracts/auth-session.contract.js.map +1 -1
  22. package/dist/contracts/index.js +13 -1
  23. package/dist/contracts/index.js.map +1 -1
  24. package/dist/contradictions/index.d.ts +1 -0
  25. package/dist/contradictions/index.js +206 -40
  26. package/dist/contradictions/index.js.map +1 -1
  27. package/dist/control-plane.d.ts +69 -0
  28. package/dist/control-plane.js +656 -0
  29. package/dist/control-plane.js.map +1 -0
  30. package/dist/coreClient.js.map +1 -1
  31. package/dist/decisions/index.d.ts +1 -0
  32. package/dist/decisions/index.js +206 -40
  33. package/dist/decisions/index.js.map +1 -1
  34. package/dist/decisionsClient.js.map +1 -1
  35. package/dist/edges/index.d.ts +1 -0
  36. package/dist/edges/index.js +206 -40
  37. package/dist/edges/index.js.map +1 -1
  38. package/dist/embeddingsClient.js.map +1 -1
  39. package/dist/eventingClient.js.map +1 -1
  40. package/dist/eventsCore.js.map +1 -1
  41. package/dist/evidence/index.d.ts +1 -0
  42. package/dist/evidence/index.js +206 -40
  43. package/dist/evidence/index.js.map +1 -1
  44. package/dist/evidenceClient.js.map +1 -1
  45. package/dist/functionSurface.d.ts +2 -1
  46. package/dist/functionSurface.js +5 -0
  47. package/dist/functionSurface.js.map +1 -1
  48. package/dist/functionSurfaceClient.js +5 -0
  49. package/dist/functionSurfaceClient.js.map +1 -1
  50. package/dist/gatewayFacades.d.ts +26 -2
  51. package/dist/gatewayFacades.js +135 -7
  52. package/dist/gatewayFacades.js.map +1 -1
  53. package/dist/graphAnalysisClient.js.map +1 -1
  54. package/dist/graphClient.js.map +1 -1
  55. package/dist/graphRecommendationsClient.js.map +1 -1
  56. package/dist/graphStateClassifierClient.js.map +1 -1
  57. package/dist/harnessClient.js.map +1 -1
  58. package/dist/identityClient.d.ts +19 -1
  59. package/dist/identityClient.js +133 -5
  60. package/dist/identityClient.js.map +1 -1
  61. package/dist/index.d.ts +1 -0
  62. package/dist/index.js +232 -49
  63. package/dist/index.js.map +1 -1
  64. package/dist/jobsClient.js.map +1 -1
  65. package/dist/learningClient.js.map +1 -1
  66. package/dist/lenses/index.d.ts +1 -0
  67. package/dist/lenses/index.js +206 -40
  68. package/dist/lenses/index.js.map +1 -1
  69. package/dist/mcpClient.js.map +1 -1
  70. package/dist/modelRuntimeClient.js.map +1 -1
  71. package/dist/nodes/index.d.ts +1 -0
  72. package/dist/nodes/index.js +206 -40
  73. package/dist/nodes/index.js.map +1 -1
  74. package/dist/ontologies/index.d.ts +1 -0
  75. package/dist/ontologies/index.js +206 -40
  76. package/dist/ontologies/index.js.map +1 -1
  77. package/dist/ontologyClient.js.map +1 -1
  78. package/dist/ontologyLinksClient.js.map +1 -1
  79. package/dist/orgGraphSearchClient.js.map +1 -1
  80. package/dist/packsClient.js.map +1 -1
  81. package/dist/policyClient.js.map +1 -1
  82. package/dist/questions/index.d.ts +1 -0
  83. package/dist/questions/index.js +206 -40
  84. package/dist/questions/index.js.map +1 -1
  85. package/dist/reportsClient.js.map +1 -1
  86. package/dist/schemaClient.js.map +1 -1
  87. package/dist/sourcesClient.js.map +1 -1
  88. package/dist/telemetryClient.js.map +1 -1
  89. package/dist/toolRegistryClient.js.map +1 -1
  90. package/dist/topics/index.d.ts +1 -0
  91. package/dist/topics/index.js +206 -40
  92. package/dist/topics/index.js.map +1 -1
  93. package/dist/topicsClient.js.map +1 -1
  94. package/dist/types.d.ts +12 -0
  95. package/dist/version.d.ts +1 -1
  96. package/dist/version.js +1 -1
  97. package/dist/version.js.map +1 -1
  98. package/dist/workflowClient.js.map +1 -1
  99. package/dist/worktrees/index.d.ts +1 -0
  100. package/dist/worktrees/index.js +206 -40
  101. package/dist/worktrees/index.js.map +1 -1
  102. package/package.json +9 -5
package/README.md CHANGED
@@ -67,6 +67,57 @@ const identity = await lucern.identity.whoami();
67
67
  const principal: SdkPrincipalContext = identity.data;
68
68
  ```
69
69
 
70
+ ### Control-Plane Tenant Bootstrap
71
+
72
+ Interactive tenant applications should resolve Clerk users through the Lucern
73
+ control-plane identity surface before making workspace-scoped graph calls. Clerk
74
+ proves the browser user's identity; Lucern authorization comes from the
75
+ Permit-backed control-plane projection.
76
+
77
+ ```typescript
78
+ import { createLucernClient } from "@lucern/sdk";
79
+
80
+ async function createLucernForClerkUser(args: {
81
+ clerkUserId: string;
82
+ getClerkToken: () => Promise<string | null>;
83
+ tenantId: string;
84
+ workspaceId: string;
85
+ clerkProjectId?: string;
86
+ }) {
87
+ const token = await args.getClerkToken();
88
+ if (!token) {
89
+ throw new Error("Clerk session token is required.");
90
+ }
91
+
92
+ const getAuthHeaders = () => ({ Authorization: `Bearer ${token}` });
93
+ const lucern = createLucernClient({
94
+ baseUrl: "https://api.lucern.ai",
95
+ getAuthHeaders,
96
+ });
97
+
98
+ const principal =
99
+ await lucern.controlPlane.identity.resolveInteractivePrincipal({
100
+ clerkId: args.clerkUserId,
101
+ tenantId: args.tenantId,
102
+ workspaceId: args.workspaceId,
103
+ providerProjectId: args.clerkProjectId,
104
+ });
105
+
106
+ return createLucernClient({
107
+ baseUrl: "https://api.lucern.ai",
108
+ getAuthHeaders,
109
+ authContext: principal.data,
110
+ });
111
+ }
112
+ ```
113
+
114
+ Use `authContext.principalId`, roles, scopes, groups, permitted tools, and
115
+ Permit subject data as the runtime Lucern principal context. Tenant apps must
116
+ not read legacy `users.mcRole` / `defaultTenantId` fields as authorization, and
117
+ they must not call `components.controlPlane.migration` from application code.
118
+ Provisioning and backfills can use migration APIs; runtime bootstrapping uses
119
+ `controlPlane.identity.resolveInteractivePrincipal(...)`.
120
+
70
121
  ## The Full Developer Journey
71
122
 
72
123
  This walkthrough mirrors what a developer building an AI-powered code review system would experience in a real coding session. Every API call is something you would actually use.
@@ -3,6 +3,7 @@ import { GatewayClientConfig } from './coreClient.js';
3
3
  import { PolicyEvaluationInput, PolicyDecisionRecord } from './identityClient.js';
4
4
  import { SessionPrincipalType } from './contracts/auth-session.contract.js';
5
5
  import { JsonObject } from './types.js';
6
+ import './control-plane.js';
6
7
  import './contracts/workflow-runtime.contract.js';
7
8
  import './contracts/lens-workflow.contract.js';
8
9
  import './contracts/lens-filter.contract.js';
@@ -29,14 +29,14 @@ function requireString(value, reason, label) {
29
29
  }
30
30
  return normalized;
31
31
  }
32
- function requirePrincipalType(principalType) {
33
- if (!principalType) {
32
+ function requirePrincipalType(principalType2) {
33
+ if (!principalType2) {
34
34
  throw new LucernSdkAuthContextError(
35
35
  "principal_missing",
36
36
  "Canonical Lucern SDK auth context is missing principalType."
37
37
  );
38
38
  }
39
- return principalType;
39
+ return principalType2;
40
40
  }
41
41
  function requireAuthMode(authMode) {
42
42
  if (!authMode) {
@@ -82,7 +82,7 @@ function normalizeCanonicalLucernAuthContext(input) {
82
82
  );
83
83
  const roles = cleanStringList(input.roles);
84
84
  const scopes = cleanStringList(input.scopes);
85
- const principalType = requirePrincipalType(input.principalType);
85
+ const principalType2 = requirePrincipalType(input.principalType);
86
86
  const authMode = requireAuthMode(input.authMode);
87
87
  const roleBasedInteractiveAuth = authMode === "interactive_user" && roles.length > 0;
88
88
  if (roles.length === 0 || scopes.length === 0 && !roleBasedInteractiveAuth) {
@@ -111,7 +111,7 @@ function normalizeCanonicalLucernAuthContext(input) {
111
111
  principalId,
112
112
  tenantId,
113
113
  workspaceId,
114
- principalType,
114
+ principalType: principalType2,
115
115
  authMode,
116
116
  roles,
117
117
  scopes,
@@ -600,6 +600,109 @@ function listResultFromEnvelope(data, legacyKey) {
600
600
  );
601
601
  }
602
602
 
603
+ // src/control-plane.ts
604
+ var LucernControlPlaneIdentityError = class extends Error {
605
+ reason;
606
+ principalStatus;
607
+ tenantStatus;
608
+ workspaceStatus;
609
+ details;
610
+ constructor(failure) {
611
+ super(failure.message);
612
+ this.name = "LucernControlPlaneIdentityError";
613
+ this.reason = failure.reason;
614
+ this.principalStatus = failure.principalStatus;
615
+ this.tenantStatus = failure.tenantStatus;
616
+ this.workspaceStatus = failure.workspaceStatus;
617
+ this.details = failure.details;
618
+ }
619
+ };
620
+ function cleanString2(value) {
621
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
622
+ }
623
+ function stringList(value) {
624
+ if (!Array.isArray(value)) {
625
+ return [];
626
+ }
627
+ return [
628
+ ...new Set(
629
+ value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean)
630
+ )
631
+ ];
632
+ }
633
+ function principalType(value) {
634
+ switch (value) {
635
+ case "service":
636
+ case "service_principal":
637
+ return "service";
638
+ case "agent":
639
+ return "agent";
640
+ case "group":
641
+ return "group";
642
+ case "external_viewer":
643
+ case "external_stakeholder":
644
+ return "external_viewer";
645
+ default:
646
+ return "human";
647
+ }
648
+ }
649
+ function adminFlags(roles) {
650
+ const normalized = roles.map((role) => role.toLowerCase());
651
+ const isPlatformAdmin = normalized.includes("platform_admin");
652
+ const isTenantAdmin = isPlatformAdmin || normalized.includes("tenant_admin");
653
+ const isWorkspaceAdmin = isTenantAdmin || normalized.includes("workspace_admin") || normalized.includes("workspace_owner");
654
+ return { isPlatformAdmin, isTenantAdmin, isWorkspaceAdmin };
655
+ }
656
+ function normalizeResolvedInteractivePrincipal(payload) {
657
+ if ("ok" in payload && payload.ok === false) {
658
+ throw new LucernControlPlaneIdentityError(payload);
659
+ }
660
+ const principalId = cleanString2(payload.principalId);
661
+ const clerkId = cleanString2(payload.clerkId);
662
+ const tenantId = cleanString2(payload.tenantId);
663
+ if (!principalId || !clerkId || !tenantId) {
664
+ throw new LucernControlPlaneIdentityError({
665
+ ok: false,
666
+ reason: "resolver_unavailable",
667
+ message: "Control-plane principal resolver returned an incomplete principal context.",
668
+ principalStatus: payload.principalStatus ?? "missing",
669
+ tenantStatus: payload.tenantStatus,
670
+ workspaceStatus: payload.workspaceStatus
671
+ });
672
+ }
673
+ const roles = stringList(payload.roles);
674
+ const scopes = stringList(payload.scopes);
675
+ const workspaceId = cleanString2(payload.workspaceId) ?? null;
676
+ const flags = adminFlags(roles);
677
+ return {
678
+ principalId,
679
+ principalType: principalType(payload.principalType),
680
+ clerkId,
681
+ tenantId,
682
+ workspaceId,
683
+ roles,
684
+ scopes,
685
+ groupIds: stringList(payload.groupIds),
686
+ permittedToolNames: stringList(payload.permittedToolNames),
687
+ permittedPackKeys: stringList(payload.permittedPackKeys),
688
+ principalStatus: cleanString2(payload.principalStatus) ?? "active",
689
+ tenantStatus: cleanString2(payload.tenantStatus) ?? "active",
690
+ workspaceStatus: cleanString2(payload.workspaceStatus) ?? (workspaceId ? "active" : "none"),
691
+ isPlatformAdmin: typeof payload.isPlatformAdmin === "boolean" ? payload.isPlatformAdmin : flags.isPlatformAdmin,
692
+ isTenantAdmin: typeof payload.isTenantAdmin === "boolean" ? payload.isTenantAdmin : flags.isTenantAdmin,
693
+ isWorkspaceAdmin: typeof payload.isWorkspaceAdmin === "boolean" ? payload.isWorkspaceAdmin : flags.isWorkspaceAdmin,
694
+ permit: {
695
+ subject: cleanString2(payload.permit?.subject) ?? principalId,
696
+ tenant: cleanString2(payload.permit?.tenant) ?? tenantId,
697
+ ...workspaceId ? { workspace: cleanString2(payload.permit?.workspace) ?? workspaceId } : {}
698
+ },
699
+ authMode: "interactive_user",
700
+ sessionId: payload.sessionId,
701
+ delegatedBy: payload.delegatedBy,
702
+ expiresAt: payload.expiresAt
703
+ };
704
+ }
705
+
603
706
  // src/identityClient.ts
604
707
  function createIdentityWhoamiClient(config = {}) {
605
708
  const gateway = createGatewayRequestClient(config);
@@ -667,13 +770,25 @@ function createIdentityClient(config = {}) {
667
770
  (response) => mapGatewayData(response, (data) => ({
668
771
  principalId: data.principalId,
669
772
  principalType: data.principalType,
773
+ clerkId: data.clerkId,
670
774
  tenantId: data.tenantId ?? null,
671
775
  workspaceId: data.workspaceId ?? null,
672
776
  scopes: Array.isArray(data.scopes) ? data.scopes : [],
673
777
  roles: Array.isArray(data.roles) ? data.roles : [],
778
+ groupIds: Array.isArray(data.groupIds) ? data.groupIds : [],
779
+ permittedToolNames: Array.isArray(data.permittedToolNames) ? data.permittedToolNames : [],
780
+ permittedPackKeys: Array.isArray(data.permittedPackKeys) ? data.permittedPackKeys : [],
781
+ principalStatus: data.principalStatus,
782
+ tenantStatus: data.tenantStatus,
783
+ workspaceStatus: data.workspaceStatus,
674
784
  isPlatformAdmin: data.isPlatformAdmin === true,
675
785
  isTenantAdmin: data.isTenantAdmin === true,
676
786
  isWorkspaceAdmin: data.isWorkspaceAdmin === true,
787
+ permit: data.permit ?? (data.tenantId ? {
788
+ subject: data.principalId,
789
+ tenant: data.tenantId,
790
+ ...data.workspaceId ? { workspace: data.workspaceId } : {}
791
+ } : void 0),
677
792
  authMode: data.authMode,
678
793
  sessionId: data.sessionId,
679
794
  delegatedBy: data.delegatedBy,
@@ -681,6 +796,19 @@ function createIdentityClient(config = {}) {
681
796
  }))
682
797
  );
683
798
  },
799
+ /**
800
+ * Resolve a Clerk subject through the tenant control-plane Permit projection.
801
+ * @deprecated Prefer lucern.controlPlane.identity.resolveInteractivePrincipal().
802
+ */
803
+ async resolveInteractivePrincipal(input) {
804
+ return gateway.request({
805
+ path: "/api/platform/v1/control-plane/identity/resolve-interactive-principal",
806
+ method: "POST",
807
+ body: input
808
+ }).then(
809
+ (response) => mapGatewayData(response, normalizeResolvedInteractivePrincipal)
810
+ );
811
+ },
684
812
  /**
685
813
  * List principals in the current identity scope.
686
814
  */
@@ -877,7 +1005,7 @@ var LucernAccessControlError = class extends LucernSdkAuthContextError {
877
1005
  this.policyDecision = policyDecision;
878
1006
  }
879
1007
  };
880
- function cleanString2(value) {
1008
+ function cleanString3(value) {
881
1009
  const normalized = value?.trim();
882
1010
  return normalized ? normalized : void 0;
883
1011
  }
@@ -892,7 +1020,7 @@ function cleanStringList2(values) {
892
1020
  ];
893
1021
  }
894
1022
  function requireString2(value, reason, label) {
895
- const normalized = cleanString2(value);
1023
+ const normalized = cleanString3(value);
896
1024
  if (!normalized) {
897
1025
  throw new LucernAccessControlError(
898
1026
  reason,
@@ -901,13 +1029,19 @@ function requireString2(value, reason, label) {
901
1029
  }
902
1030
  return normalized;
903
1031
  }
904
- function normalizePrincipalType(principalType) {
905
- if (principalType === "agent") {
1032
+ function normalizePrincipalType(principalType2) {
1033
+ if (principalType2 === "agent") {
906
1034
  return "agent";
907
1035
  }
908
- if (principalType === "service") {
1036
+ if (principalType2 === "service") {
909
1037
  return "service";
910
1038
  }
1039
+ if (principalType2 === "group") {
1040
+ return "group";
1041
+ }
1042
+ if (principalType2 === "external_viewer") {
1043
+ return "external_viewer";
1044
+ }
911
1045
  return "human";
912
1046
  }
913
1047
  function aliasKey(alias) {
@@ -916,15 +1050,15 @@ function aliasKey(alias) {
916
1050
  function normalizeAliases(input, canonicalClerkUserId) {
917
1051
  const aliases = /* @__PURE__ */ new Map();
918
1052
  for (const alias of input ?? []) {
919
- const externalSubjectId = cleanString2(alias.externalSubjectId);
1053
+ const externalSubjectId = cleanString3(alias.externalSubjectId);
920
1054
  if (!externalSubjectId) {
921
1055
  continue;
922
1056
  }
923
1057
  const normalized = {
924
- provider: cleanString2(alias.provider) ?? "clerk",
925
- providerProjectId: cleanString2(alias.providerProjectId),
1058
+ provider: cleanString3(alias.provider) ?? "clerk",
1059
+ providerProjectId: cleanString3(alias.providerProjectId),
926
1060
  externalSubjectId,
927
- status: cleanString2(alias.status)
1061
+ status: cleanString3(alias.status)
928
1062
  };
929
1063
  aliases.set(aliasKey(normalized), normalized);
930
1064
  }
@@ -969,10 +1103,10 @@ function normalizeCanonicalPrincipalIdentity(input, options = {}) {
969
1103
  "principal_missing",
970
1104
  "principalId"
971
1105
  );
972
- const principalType = normalizePrincipalType(principalInput.principalType);
973
- const observedClerkId = cleanString2(options.observedClerkId);
974
- const canonicalClerkUserId = cleanString2(principalInput.canonicalClerkUserId) ?? cleanString2(principalInput.clerkId);
975
- if (principalType === "human" && !canonicalClerkUserId) {
1106
+ const principalType2 = normalizePrincipalType(principalInput.principalType);
1107
+ const observedClerkId = cleanString3(options.observedClerkId);
1108
+ const canonicalClerkUserId = cleanString3(principalInput.canonicalClerkUserId) ?? cleanString3(principalInput.clerkId);
1109
+ if (principalType2 === "human" && !canonicalClerkUserId) {
976
1110
  throw new LucernAccessControlError(
977
1111
  "clerk_alias_missing",
978
1112
  "Human principals require one canonical Clerk user id."
@@ -994,11 +1128,11 @@ function normalizeCanonicalPrincipalIdentity(input, options = {}) {
994
1128
  }
995
1129
  return {
996
1130
  principalId,
997
- principalType,
1131
+ principalType: principalType2,
998
1132
  canonicalClerkUserId,
999
1133
  clerkIdentityAliases: aliases,
1000
- tenantId: cleanString2(principalInput.tenantId),
1001
- workspaceId: cleanString2(principalInput.workspaceId),
1134
+ tenantId: cleanString3(principalInput.tenantId),
1135
+ workspaceId: cleanString3(principalInput.workspaceId),
1002
1136
  roles: cleanStringList2(principalInput.roles),
1003
1137
  scopes: cleanStringList2(principalInput.scopes)
1004
1138
  };
@@ -1023,7 +1157,7 @@ function buildPolicyInput(identity, input) {
1023
1157
  "tenant_missing",
1024
1158
  "tenantId"
1025
1159
  );
1026
- const workspaceId = cleanString2(input.workspaceId ?? identity.workspaceId);
1160
+ const workspaceId = cleanString3(input.workspaceId ?? identity.workspaceId);
1027
1161
  if (resourceRequiresWorkspace(input.resource) && !workspaceId) {
1028
1162
  throw new LucernAccessControlError(
1029
1163
  "workspace_missing",