@sixb/core 0.1.13 → 0.1.14

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 (97) hide show
  1. package/dist/actions/index.js +9 -9
  2. package/dist/agents/index.js +16 -16
  3. package/dist/auth/errors.d.ts +12 -0
  4. package/dist/auth/errors.d.ts.map +1 -1
  5. package/dist/auth/index.d.ts +1 -1
  6. package/dist/auth/index.d.ts.map +1 -1
  7. package/dist/auth/index.js +5 -3
  8. package/dist/auth/index.js.map +1 -1
  9. package/dist/auth/runtime.d.ts.map +1 -1
  10. package/dist/auth/strategy.d.ts +2 -0
  11. package/dist/auth/strategy.d.ts.map +1 -1
  12. package/dist/auth/strategy.js +4 -2
  13. package/dist/auth/strategy.js.map +1 -1
  14. package/dist/auth/types.d.ts +8 -0
  15. package/dist/auth/types.d.ts.map +1 -1
  16. package/dist/bootstrap/index.js +20 -20
  17. package/dist/{chunk-rqtdtm43.js → chunk-00pfcqh4.js} +1 -1
  18. package/dist/{chunk-cvrbhvtr.js → chunk-16jenmzw.js} +10 -10
  19. package/dist/{chunk-0321g649.js → chunk-2jntasmh.js} +13 -2
  20. package/dist/{chunk-0321g649.js.map → chunk-2jntasmh.js.map} +3 -3
  21. package/dist/{chunk-n33e8dz8.js → chunk-3a338wrp.js} +1 -1
  22. package/dist/{chunk-gc226ten.js → chunk-3fpgxpsq.js} +3 -3
  23. package/dist/{chunk-gmt22nxx.js → chunk-4ypbe95r.js} +1 -1
  24. package/dist/{chunk-9pfw47qx.js → chunk-6rzb1g6h.js} +52 -47
  25. package/dist/{chunk-9pfw47qx.js.map → chunk-6rzb1g6h.js.map} +7 -7
  26. package/dist/{chunk-898y24dd.js → chunk-728fp090.js} +1 -1
  27. package/dist/{chunk-693f3zn8.js → chunk-dv4ct8mp.js} +2 -2
  28. package/dist/{chunk-jhyc777j.js → chunk-e2c5yxfe.js} +1 -1
  29. package/dist/{chunk-darm7eeg.js → chunk-gct3m1da.js} +1 -1
  30. package/dist/{chunk-mf6hwpzh.js → chunk-hj0n3c2p.js} +12 -12
  31. package/dist/{chunk-91wthfwe.js → chunk-hxgj4kzd.js} +4 -4
  32. package/dist/{chunk-w9bp61e7.js → chunk-jfdggg1p.js} +1 -1
  33. package/dist/{chunk-04h4t2b5.js → chunk-kxea54yg.js} +1 -1
  34. package/dist/{chunk-jmwy25mf.js → chunk-ma1mhh7w.js} +5 -5
  35. package/dist/{chunk-63fp9x3q.js → chunk-pvgpckm1.js} +2 -2
  36. package/dist/{chunk-a69fkrtr.js → chunk-q2xghyft.js} +1 -1
  37. package/dist/{chunk-s24qbp32.js → chunk-wn7mznxc.js} +2 -2
  38. package/dist/{chunk-gbcrz0gr.js → chunk-wnxabtn4.js} +2 -2
  39. package/dist/{chunk-bep5rrdq.js → chunk-ww185kt6.js} +1 -1
  40. package/dist/{chunk-2hbhn1ff.js → chunk-yvf2jqj8.js} +1 -1
  41. package/dist/execution/agent.js +16 -16
  42. package/dist/execution/primitive.js +16 -16
  43. package/dist/execution/request.js +16 -16
  44. package/dist/index.js +22 -22
  45. package/dist/materializer/index.js +6 -6
  46. package/dist/models/execution/index.js +6 -6
  47. package/dist/objects/index.js +9 -9
  48. package/dist/objects/query/index.js +6 -6
  49. package/dist/pipelines/index.js +7 -7
  50. package/dist/projections/internal.js +5 -5
  51. package/dist/runtime/internal.js +8 -8
  52. package/dist/shares/internal.js +6 -6
  53. package/dist/storage/auth/errors.d.ts +1 -1
  54. package/dist/storage/auth/errors.d.ts.map +1 -1
  55. package/dist/storage/auth/in-memory/auth-storage.d.ts +2 -1
  56. package/dist/storage/auth/in-memory/auth-storage.d.ts.map +1 -1
  57. package/dist/storage/auth/types.d.ts +15 -7
  58. package/dist/storage/auth/types.d.ts.map +1 -1
  59. package/dist/storage/file-upload-sessions/provider.js +3 -3
  60. package/dist/storage/index.js +5 -5
  61. package/dist/storage/workflow-runs/provider.js +3 -3
  62. package/dist/syncs/index.js +7 -7
  63. package/dist/testing/auth-storage-contract.d.ts.map +1 -1
  64. package/dist/testing/index.js +107 -139
  65. package/dist/testing/index.js.map +3 -3
  66. package/dist/webhooks/internal.js +5 -5
  67. package/dist/workflows/index.js +17 -17
  68. package/package.json +1 -1
  69. package/src/auth/errors.ts +23 -0
  70. package/src/auth/index.ts +1 -1
  71. package/src/auth/runtime.ts +56 -22
  72. package/src/auth/strategy.ts +2 -0
  73. package/src/auth/types.ts +8 -0
  74. package/src/storage/auth/errors.ts +0 -1
  75. package/src/storage/auth/in-memory/auth-storage.ts +44 -65
  76. package/src/storage/auth/types.ts +15 -7
  77. package/src/testing/auth-storage-contract.ts +104 -130
  78. /package/dist/{chunk-rqtdtm43.js.map → chunk-00pfcqh4.js.map} +0 -0
  79. /package/dist/{chunk-cvrbhvtr.js.map → chunk-16jenmzw.js.map} +0 -0
  80. /package/dist/{chunk-n33e8dz8.js.map → chunk-3a338wrp.js.map} +0 -0
  81. /package/dist/{chunk-gc226ten.js.map → chunk-3fpgxpsq.js.map} +0 -0
  82. /package/dist/{chunk-gmt22nxx.js.map → chunk-4ypbe95r.js.map} +0 -0
  83. /package/dist/{chunk-898y24dd.js.map → chunk-728fp090.js.map} +0 -0
  84. /package/dist/{chunk-693f3zn8.js.map → chunk-dv4ct8mp.js.map} +0 -0
  85. /package/dist/{chunk-jhyc777j.js.map → chunk-e2c5yxfe.js.map} +0 -0
  86. /package/dist/{chunk-darm7eeg.js.map → chunk-gct3m1da.js.map} +0 -0
  87. /package/dist/{chunk-mf6hwpzh.js.map → chunk-hj0n3c2p.js.map} +0 -0
  88. /package/dist/{chunk-91wthfwe.js.map → chunk-hxgj4kzd.js.map} +0 -0
  89. /package/dist/{chunk-w9bp61e7.js.map → chunk-jfdggg1p.js.map} +0 -0
  90. /package/dist/{chunk-04h4t2b5.js.map → chunk-kxea54yg.js.map} +0 -0
  91. /package/dist/{chunk-jmwy25mf.js.map → chunk-ma1mhh7w.js.map} +0 -0
  92. /package/dist/{chunk-63fp9x3q.js.map → chunk-pvgpckm1.js.map} +0 -0
  93. /package/dist/{chunk-a69fkrtr.js.map → chunk-q2xghyft.js.map} +0 -0
  94. /package/dist/{chunk-s24qbp32.js.map → chunk-wn7mznxc.js.map} +0 -0
  95. /package/dist/{chunk-gbcrz0gr.js.map → chunk-wnxabtn4.js.map} +0 -0
  96. /package/dist/{chunk-bep5rrdq.js.map → chunk-ww185kt6.js.map} +0 -0
  97. /package/dist/{chunk-2hbhn1ff.js.map → chunk-yvf2jqj8.js.map} +0 -0
@@ -10,9 +10,9 @@ import {
10
10
  } from "../chunk-cbn96bve.js";
11
11
  import"../chunk-xgptywp0.js";
12
12
  import"../chunk-fc5zfxy1.js";
13
- import"../chunk-9pfw47qx.js";
13
+ import"../chunk-6rzb1g6h.js";
14
14
  import"../chunk-drz59ve0.js";
15
- import"../chunk-0321g649.js";
15
+ import"../chunk-2jntasmh.js";
16
16
  import"../chunk-pqajwytd.js";
17
17
  import"../chunk-ky7zpbtt.js";
18
18
  import"../chunk-gfdh9kqa.js";
@@ -21,7 +21,7 @@ import"../chunk-69266efg.js";
21
21
  import"../chunk-xnx96gfy.js";
22
22
  import"../chunk-jgwb25gc.js";
23
23
  import"../chunk-h89wz928.js";
24
- import"../chunk-n33e8dz8.js";
24
+ import"../chunk-3a338wrp.js";
25
25
  import {
26
26
  ExecutionStorageError2
27
27
  } from "../chunk-tw03bsmb.js";
@@ -29,7 +29,7 @@ import"../chunk-3nx8vj5a.js";
29
29
  import"../chunk-ht4a7mjg.js";
30
30
  import {
31
31
  canRetryWebhookRun2
32
- } from "../chunk-gc226ten.js";
32
+ } from "../chunk-3fpgxpsq.js";
33
33
  import"../chunk-7ytf06d3.js";
34
34
  import"../chunk-mr60an99.js";
35
35
  import"../chunk-mmagke4x.js";
@@ -39,7 +39,7 @@ import"../chunk-jmfet9r4.js";
39
39
  import {
40
40
  WebhookRunError2
41
41
  } from "../chunk-qf954wje.js";
42
- import"../chunk-darm7eeg.js";
42
+ import"../chunk-gct3m1da.js";
43
43
  import {
44
44
  isOAuthConnectorDefinition2,
45
45
  isStaticConnectorDefinition2
@@ -8,7 +8,7 @@ import {
8
8
  defineWorkflow2,
9
9
  createWorkflowNodeFailure2,
10
10
  unwrapWorkflowNodeFailure2
11
- } from "../chunk-s24qbp32.js";
11
+ } from "../chunk-wn7mznxc.js";
12
12
  import"../chunk-e1c18xjx.js";
13
13
  import"../chunk-fb3feg1e.js";
14
14
  import"../chunk-dfm066qg.js";
@@ -45,31 +45,31 @@ import {
45
45
  snapshotWorkflowActionInput2,
46
46
  WorkflowRunDispatcher2,
47
47
  requestWorkflowRun2
48
- } from "../chunk-cvrbhvtr.js";
49
- import"../chunk-a69fkrtr.js";
48
+ } from "../chunk-16jenmzw.js";
49
+ import"../chunk-q2xghyft.js";
50
50
  import"../chunk-cbn96bve.js";
51
- import"../chunk-bep5rrdq.js";
52
- import"../chunk-gmt22nxx.js";
53
- import"../chunk-91wthfwe.js";
51
+ import"../chunk-ww185kt6.js";
52
+ import"../chunk-4ypbe95r.js";
53
+ import"../chunk-hxgj4kzd.js";
54
54
  import"../chunk-85cd7e8c.js";
55
- import"../chunk-w9bp61e7.js";
55
+ import"../chunk-jfdggg1p.js";
56
56
  import"../chunk-bxx3ms5t.js";
57
- import"../chunk-04h4t2b5.js";
57
+ import"../chunk-kxea54yg.js";
58
58
  import"../chunk-f85hpqnm.js";
59
59
  import"../chunk-4tnrzqqh.js";
60
60
  import"../chunk-5a4a52tq.js";
61
61
  import"../chunk-91kcnbf0.js";
62
62
  import"../chunk-rb143ggb.js";
63
63
  import"../chunk-cd234ba4.js";
64
- import"../chunk-693f3zn8.js";
65
- import"../chunk-63fp9x3q.js";
66
- import"../chunk-898y24dd.js";
67
- import"../chunk-rqtdtm43.js";
64
+ import"../chunk-dv4ct8mp.js";
65
+ import"../chunk-pvgpckm1.js";
66
+ import"../chunk-728fp090.js";
67
+ import"../chunk-00pfcqh4.js";
68
68
  import"../chunk-m8zkvep1.js";
69
- import"../chunk-gc226ten.js";
69
+ import"../chunk-3fpgxpsq.js";
70
70
  import"../chunk-jmfet9r4.js";
71
71
  import"../chunk-qf954wje.js";
72
- import"../chunk-darm7eeg.js";
72
+ import"../chunk-gct3m1da.js";
73
73
  import"../chunk-zdkcdshk.js";
74
74
  import"../chunk-ht4a7mjg.js";
75
75
  import"../chunk-mr60an99.js";
@@ -83,7 +83,7 @@ import"../chunk-xnx96gfy.js";
83
83
  import"../chunk-s3csryxe.js";
84
84
  import"../chunk-jgwb25gc.js";
85
85
  import"../chunk-tw03bsmb.js";
86
- import"../chunk-n33e8dz8.js";
86
+ import"../chunk-3a338wrp.js";
87
87
  import"../chunk-h89wz928.js";
88
88
  import"../chunk-xgptywp0.js";
89
89
  import"../chunk-ky7zpbtt.js";
@@ -97,7 +97,7 @@ import"../chunk-gfdh9kqa.js";
97
97
  import"../chunk-wr8qp979.js";
98
98
  import"../chunk-dmn70ztd.js";
99
99
  import"../chunk-vn3yh259.js";
100
- import"../chunk-9pfw47qx.js";
100
+ import"../chunk-6rzb1g6h.js";
101
101
  import"../chunk-2fv8sajb.js";
102
102
  import"../chunk-drz59ve0.js";
103
103
  import"../chunk-rzcjp5m2.js";
@@ -105,7 +105,7 @@ import"../chunk-v4jbb3rs.js";
105
105
  import"../chunk-d8yes9yx.js";
106
106
  import"../chunk-rzkr70gr.js";
107
107
  import"../chunk-z3y5kg1x.js";
108
- import"../chunk-0321g649.js";
108
+ import"../chunk-2jntasmh.js";
109
109
  export {
110
110
  WorkflowDefinitionError2 as WorkflowDefinitionError,
111
111
  WorkflowRunDispatcher2 as WorkflowRunDispatcher,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixb/core",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "The ontology-first runtime for Sixb — objects, links, telemetry, actions, and provider contracts",
5
5
  "keywords": [
6
6
  "sixb",
@@ -17,3 +17,26 @@ export class AuthRuntimeError extends Error {
17
17
  super(message)
18
18
  }
19
19
  }
20
+
21
+ /** Why a strategy refused someone who authenticated. Each is something a person can act on. */
22
+ export type SignInRefusalReason =
23
+ | "not_invited"
24
+ | "suspended"
25
+ | "domain_not_allowed"
26
+ | "no_trusted_address"
27
+
28
+ /**
29
+ * Thrown by a strategy that refuses a sign-in for a known reason. The sign-in page tells the
30
+ * person which reason, naming `email` when there is one; `message` is for the server log.
31
+ */
32
+ export class SignInRefusedError extends Error {
33
+ readonly name = "SignInRefusedError"
34
+
35
+ constructor(
36
+ readonly reason: SignInRefusalReason,
37
+ message: string,
38
+ readonly email?: string
39
+ ) {
40
+ super(message)
41
+ }
42
+ }
package/src/auth/index.ts CHANGED
@@ -37,7 +37,7 @@ export {
37
37
  verifyDoubleSubmitCsrf,
38
38
  } from "./csrf"
39
39
  export type { AuthRuntimeErrorCode } from "./errors"
40
- export { AuthRuntimeError } from "./errors"
40
+ export { AuthRuntimeError, SignInRefusedError } from "./errors"
41
41
  export type { AuthRuntimeOptions } from "./runtime"
42
42
  export {
43
43
  AuthRuntime,
@@ -19,6 +19,7 @@ import {
19
19
  type AuthStorage,
20
20
  AuthStorageError,
21
21
  type CompleteDeviceAuthorizationInput,
22
+ type GroupMembershipRecord,
22
23
  type InvitationRecord,
23
24
  type ServiceAccountGroupMembershipRecord,
24
25
  type ServiceAccountRecord,
@@ -991,9 +992,9 @@ export class AuthRuntime {
991
992
 
992
993
  const members: MemberSummary[] = []
993
994
  for (const user of result.users) {
994
- const groupIds = (
995
+ const { groupIds, managedGroupIds } = memberGroups(
995
996
  await storage.groupMemberships.listForUser({ projectId: this.projectId, userId: user.id })
996
- ).map((membership) => membership.groupId)
997
+ )
997
998
 
998
999
  // Visibility is scope-based: a member is listed when the caller can assign
999
1000
  // groups or suspend over the member's current groups. This keeps out-of-scope
@@ -1008,6 +1009,7 @@ export class AuthRuntime {
1008
1009
  members.push({
1009
1010
  user,
1010
1011
  groupIds,
1012
+ managedGroupIds,
1011
1013
  capabilities: {
1012
1014
  assignGroups: canAssignGroups,
1013
1015
  // A caller cannot suspend themselves; reactivation only applies to a
@@ -1033,12 +1035,11 @@ export class AuthRuntime {
1033
1035
  // The target must exist and every group it currently holds must be assignable
1034
1036
  // by the caller. Missing and out-of-scope targets raise the same error so a
1035
1037
  // caller cannot probe which users exist.
1036
- const { user, groupIds: currentGroupIds } = await this.requireManageableMember(
1037
- storage,
1038
- scope,
1039
- "assignGroups",
1040
- userId
1041
- )
1038
+ const {
1039
+ user,
1040
+ groupIds: currentGroupIds,
1041
+ managedGroupIds: currentManagedGroupIds,
1042
+ } = await this.requireManageableMember(storage, scope, "assignGroups", userId)
1042
1043
 
1043
1044
  const requestedGroupIds = this.resolveMemberGroupIds(input.groupIds)
1044
1045
  // Every requested group must fall within the caller's assign scope.
@@ -1055,6 +1056,15 @@ export class AuthRuntime {
1055
1056
  const additions = requestedGroupIds.filter((groupId) => !current.has(groupId))
1056
1057
  const removals = currentGroupIds.filter((groupId) => !requested.has(groupId))
1057
1058
 
1059
+ // The identity provider would restore these at the user's next sign-in.
1060
+ const managedRemovals = removals.filter((groupId) => currentManagedGroupIds.includes(groupId))
1061
+ if (managedRemovals.length > 0) {
1062
+ throw new AuthRuntimeError(
1063
+ "invalid_auth_input",
1064
+ `[Sixb] Group(s) ${managedRemovals.join(", ")} are managed by the identity provider. Change them there; they sync at the user's next sign-in.`
1065
+ )
1066
+ }
1067
+
1058
1068
  // Self-protection: a caller may add in-scope groups to themselves but may not
1059
1069
  // remove any of their own current groups, so they cannot lock themselves out.
1060
1070
  if (user.id === caller.user.id && removals.length > 0) {
@@ -1086,11 +1096,11 @@ export class AuthRuntime {
1086
1096
  // request resolves the updated membership.
1087
1097
  this.sessionCache?.invalidateUser(user.id)
1088
1098
 
1089
- const groupIds = (
1099
+ const { groupIds, managedGroupIds } = memberGroups(
1090
1100
  await storage.groupMemberships.listForUser({ projectId: this.projectId, userId: user.id })
1091
- ).map((membership) => membership.groupId)
1101
+ )
1092
1102
 
1093
- return { user, groupIds }
1103
+ return { user, groupIds, managedGroupIds }
1094
1104
  }
1095
1105
 
1096
1106
  async suspendMember(
@@ -1100,7 +1110,12 @@ export class AuthRuntime {
1100
1110
  const storage = this.requireAuthStorage()
1101
1111
  const scope = this.resolveMembershipPolicyScopeForUser(caller.groupIds)
1102
1112
  const userId = assertNonEmpty(input.userId, "User id")
1103
- const { user, groupIds } = await this.requireManageableMember(storage, scope, "suspend", userId)
1113
+ const { user, groupIds, managedGroupIds } = await this.requireManageableMember(
1114
+ storage,
1115
+ scope,
1116
+ "suspend",
1117
+ userId
1118
+ )
1104
1119
 
1105
1120
  if (user.id === caller.user.id) {
1106
1121
  throw new AuthRuntimeError(
@@ -1118,7 +1133,7 @@ export class AuthRuntime {
1118
1133
  // user stops authenticating immediately.
1119
1134
  this.sessionCache?.invalidateUser(user.id)
1120
1135
 
1121
- return { user: suspended, groupIds }
1136
+ return { user: suspended, groupIds, managedGroupIds }
1122
1137
  }
1123
1138
 
1124
1139
  async reactivateMember(
@@ -1128,7 +1143,12 @@ export class AuthRuntime {
1128
1143
  const storage = this.requireAuthStorage()
1129
1144
  const scope = this.resolveMembershipPolicyScopeForUser(caller.groupIds)
1130
1145
  const userId = assertNonEmpty(input.userId, "User id")
1131
- const { user, groupIds } = await this.requireManageableMember(storage, scope, "suspend", userId)
1146
+ const { user, groupIds, managedGroupIds } = await this.requireManageableMember(
1147
+ storage,
1148
+ scope,
1149
+ "suspend",
1150
+ userId
1151
+ )
1132
1152
 
1133
1153
  // Reactivation restores access but not sessions; the user signs in again.
1134
1154
  const reactivated = await storage.users.updateStatus({
@@ -1138,7 +1158,7 @@ export class AuthRuntime {
1138
1158
  updatedAt: new Date(),
1139
1159
  })
1140
1160
 
1141
- return { user: reactivated, groupIds }
1161
+ return { user: reactivated, groupIds, managedGroupIds }
1142
1162
  }
1143
1163
 
1144
1164
  /** The caller's active sessions across every audience, most recently active first. */
@@ -1210,13 +1230,15 @@ export class AuthRuntime {
1210
1230
  scope: MembershipPolicyScope,
1211
1231
  operation: MembershipOperation,
1212
1232
  userId: string
1213
- ): Promise<{ readonly user: UserRecord; readonly groupIds: readonly string[] }> {
1233
+ ): Promise<{
1234
+ readonly user: UserRecord
1235
+ readonly groupIds: readonly string[]
1236
+ readonly managedGroupIds: readonly string[]
1237
+ }> {
1214
1238
  const user = await storage.users.getById({ projectId: this.projectId, id: userId })
1215
- const groupIds = user
1216
- ? (await storage.groupMemberships.listForUser({ projectId: this.projectId, userId })).map(
1217
- (membership) => membership.groupId
1218
- )
1219
- : []
1239
+ const { groupIds, managedGroupIds } = memberGroups(
1240
+ user ? await storage.groupMemberships.listForUser({ projectId: this.projectId, userId }) : []
1241
+ )
1220
1242
 
1221
1243
  if (!user || !canPerformMembershipOperation(scope, operation, groupIds)) {
1222
1244
  throw new AuthStorageError(
@@ -1225,7 +1247,7 @@ export class AuthRuntime {
1225
1247
  )
1226
1248
  }
1227
1249
 
1228
- return { user, groupIds }
1250
+ return { user, groupIds, managedGroupIds }
1229
1251
  }
1230
1252
 
1231
1253
  private resolveMemberGroupIds(input: readonly string[]): readonly string[] {
@@ -1525,6 +1547,18 @@ export class AuthRuntime {
1525
1547
  }
1526
1548
  }
1527
1549
 
1550
+ function memberGroups(memberships: readonly GroupMembershipRecord[]): {
1551
+ readonly groupIds: readonly string[]
1552
+ readonly managedGroupIds: readonly string[]
1553
+ } {
1554
+ return {
1555
+ groupIds: memberships.map((membership) => membership.groupId),
1556
+ managedGroupIds: memberships
1557
+ .filter((membership) => membership.source === "directory")
1558
+ .map((membership) => membership.groupId),
1559
+ }
1560
+ }
1561
+
1528
1562
  /** The interactive session a caller authenticated with, when it used one. */
1529
1563
  function callerSessionId(caller: AuthenticatedRequestAuthSession): string | undefined {
1530
1564
  return caller.credentialSource === "session" ? caller.session.id : undefined
@@ -7,6 +7,8 @@
7
7
  * entry plus the storage contracts from `@sixb/core/storage`.
8
8
  */
9
9
 
10
+ export type { SignInRefusalReason } from "./errors"
11
+ export { SignInRefusedError } from "./errors"
10
12
  export type {
11
13
  AuthEmailDeliveryStatus,
12
14
  AuthInvitationRecipientInput,
package/src/auth/types.ts CHANGED
@@ -322,6 +322,11 @@ export interface MemberCapabilities {
322
322
  export interface MemberSummary {
323
323
  readonly user: UserRecord
324
324
  readonly groupIds: readonly string[]
325
+ /**
326
+ * The groups among `groupIds` that the identity provider manages. They sync at the user's next
327
+ * sign-in, so they cannot be removed here.
328
+ */
329
+ readonly managedGroupIds: readonly string[]
325
330
  readonly capabilities: MemberCapabilities
326
331
  }
327
332
 
@@ -340,6 +345,7 @@ export interface UpdateMemberGroupsInput {
340
345
  export interface UpdateMemberGroupsResult {
341
346
  readonly user: UserRecord
342
347
  readonly groupIds: readonly string[]
348
+ readonly managedGroupIds: readonly string[]
343
349
  }
344
350
 
345
351
  export interface SuspendMemberInput {
@@ -349,6 +355,7 @@ export interface SuspendMemberInput {
349
355
  export interface SuspendMemberResult {
350
356
  readonly user: UserRecord
351
357
  readonly groupIds: readonly string[]
358
+ readonly managedGroupIds: readonly string[]
352
359
  }
353
360
 
354
361
  export interface ReactivateMemberInput {
@@ -358,6 +365,7 @@ export interface ReactivateMemberInput {
358
365
  export interface ReactivateMemberResult {
359
366
  readonly user: UserRecord
360
367
  readonly groupIds: readonly string[]
368
+ readonly managedGroupIds: readonly string[]
361
369
  }
362
370
 
363
371
  /** `"session"` reads only the cookie; `"any"` reads a bearer access token, else the cookie. */
@@ -10,7 +10,6 @@ export type AuthStorageErrorCode =
10
10
  | "duplicate_service_account"
11
11
  | "duplicate_session"
12
12
  | "duplicate_user"
13
- | "email_link_not_allowed"
14
13
  | "expired_magic_link"
15
14
  | "expired_oidc_attempt"
16
15
  | "invalid_input"
@@ -7,6 +7,7 @@ import type {
7
7
  CompleteOidcSignInInput,
8
8
  CompleteSignInResult,
9
9
  GroupMembershipRecord,
10
+ GroupMembershipSource,
10
11
  InvitationRecord,
11
12
  SessionRecord,
12
13
  SuspendUserAndRevokeSessionsInput,
@@ -42,6 +43,7 @@ import {
42
43
  normalizeEmail,
43
44
  normalizeGroupIds,
44
45
  oidcAttemptKey,
46
+ removeGroupMembershipRecord,
45
47
  revokeActiveSessionsForUser,
46
48
  upsertGroupMembershipRecord,
47
49
  userKey,
@@ -219,25 +221,6 @@ export class InMemoryAuthStorage implements AuthStorage {
219
221
  )
220
222
  }
221
223
 
222
- if (
223
- shouldCreateUser &&
224
- !activeInvitation &&
225
- input.allowUserCreationWithoutInvitation &&
226
- input.requireNoActiveUsersForUserCreation &&
227
- hasActiveUsers(this.state, projectId)
228
- ) {
229
- consumeMagicLinkRecord(this.state, {
230
- projectId,
231
- id: input.magicLinkId,
232
- tokenHash: input.tokenHash,
233
- consumedAt: completedAt,
234
- })
235
- throw new AuthStorageError(
236
- "user_creation_not_allowed",
237
- `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`
238
- )
239
- }
240
-
241
224
  validateCompleteSessionInput(this.state, projectId, input.session)
242
225
  assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience)
243
226
 
@@ -280,8 +263,9 @@ export class InMemoryAuthStorage implements AuthStorage {
280
263
  projectId,
281
264
  user,
282
265
  })
283
- const memberships = this.applyManualGroups({
266
+ const memberships = this.applyGroups({
284
267
  completedAt,
268
+ source: "manual",
285
269
  groupIds: manualGroupIds,
286
270
  projectId,
287
271
  userId: user.id,
@@ -352,14 +336,6 @@ export class InMemoryAuthStorage implements AuthStorage {
352
336
  )
353
337
  }
354
338
 
355
- if (!identity && user && (!input.autoLinkByVerifiedEmail || !input.emailVerified)) {
356
- this.consumeOidcAttempt(input, completedAt, projectId)
357
- throw new AuthStorageError(
358
- "email_link_not_allowed",
359
- `[Sixb] OIDC identity cannot auto-link to user '${user.id}' for project '${projectId}'.`
360
- )
361
- }
362
-
363
339
  if (user?.status === "suspended") {
364
340
  this.consumeOidcAttempt(input, completedAt, projectId)
365
341
  throw new AuthStorageError(
@@ -368,14 +344,6 @@ export class InMemoryAuthStorage implements AuthStorage {
368
344
  )
369
345
  }
370
346
 
371
- if (shouldCreateUser && !input.emailVerified) {
372
- this.consumeOidcAttempt(input, completedAt, projectId)
373
- throw new AuthStorageError(
374
- "user_creation_not_allowed",
375
- `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
376
- )
377
- }
378
-
379
347
  if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
380
348
  this.consumeOidcAttempt(input, completedAt, projectId)
381
349
  throw new AuthStorageError(
@@ -384,20 +352,6 @@ export class InMemoryAuthStorage implements AuthStorage {
384
352
  )
385
353
  }
386
354
 
387
- if (
388
- shouldCreateUser &&
389
- !activeInvitation &&
390
- input.allowUserCreationWithoutInvitation &&
391
- input.requireNoActiveUsersForUserCreation &&
392
- hasActiveUsers(this.state, projectId)
393
- ) {
394
- this.consumeOidcAttempt(input, completedAt, projectId)
395
- throw new AuthStorageError(
396
- "user_creation_not_allowed",
397
- `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
398
- )
399
- }
400
-
401
355
  validateCompleteSessionInput(this.state, projectId, input.session)
402
356
  assertSignInSessionAudience(projectId, input.session.audience, attempt.audience)
403
357
 
@@ -434,12 +388,19 @@ export class InMemoryAuthStorage implements AuthStorage {
434
388
  projectId,
435
389
  user,
436
390
  })
437
- const memberships = this.applyManualGroups({
391
+ const memberships = this.syncDirectoryGroups({
438
392
  completedAt,
439
- groupIds: manualGroupIds,
393
+ groupIds: input.directoryGroupIds,
440
394
  projectId,
441
395
  userId: user.id,
442
- existing: invitation.memberships,
396
+ existing: this.applyGroups({
397
+ completedAt,
398
+ source: "manual",
399
+ groupIds: manualGroupIds,
400
+ projectId,
401
+ userId: user.id,
402
+ existing: invitation.memberships,
403
+ }),
443
404
  })
444
405
 
445
406
  const nextIdentity: UserIdentityRecord = {
@@ -533,10 +494,11 @@ export class InMemoryAuthStorage implements AuthStorage {
533
494
  return { invitation, memberships }
534
495
  }
535
496
 
536
- private applyManualGroups(input: {
497
+ private applyGroups(input: {
537
498
  readonly completedAt: Date
538
499
  readonly groupIds: readonly string[]
539
500
  readonly projectId: string
501
+ readonly source: GroupMembershipSource
540
502
  readonly userId: string
541
503
  readonly existing: readonly GroupMembershipRecord[]
542
504
  }): readonly GroupMembershipRecord[] {
@@ -547,13 +509,40 @@ export class InMemoryAuthStorage implements AuthStorage {
547
509
  projectId: input.projectId,
548
510
  userId: input.userId,
549
511
  groupId,
550
- source: "manual",
512
+ source: input.source,
551
513
  createdAt: input.completedAt,
552
514
  })
553
515
  )
554
516
  }
555
517
  return memberships
556
518
  }
519
+ // Directory memberships become exactly `groupIds`. A group the user already holds from another
520
+ // source keeps that source, and memberships from other sources are never removed.
521
+ private syncDirectoryGroups(input: {
522
+ readonly completedAt: Date
523
+ readonly groupIds?: readonly string[]
524
+ readonly projectId: string
525
+ readonly userId: string
526
+ readonly existing: readonly GroupMembershipRecord[]
527
+ }): readonly GroupMembershipRecord[] {
528
+ if (!input.groupIds) {
529
+ return input.existing
530
+ }
531
+
532
+ const groupIds = normalizeGroupIds(input.groupIds)
533
+ const stale = [...this.state.groupMemberships.values()].filter(
534
+ (membership) =>
535
+ membership.projectId === input.projectId &&
536
+ membership.userId === input.userId &&
537
+ membership.source === "directory" &&
538
+ !groupIds.includes(membership.groupId)
539
+ )
540
+ for (const membership of stale) {
541
+ removeGroupMembershipRecord(this.state, membership)
542
+ }
543
+
544
+ return this.applyGroups({ ...input, groupIds, source: "directory" })
545
+ }
557
546
 
558
547
  private createSignInSession(input: {
559
548
  readonly projectId: string
@@ -599,16 +588,6 @@ function restoreMap<TKey, TValue>(target: Map<TKey, TValue>, snapshot: Map<TKey,
599
588
  }
600
589
  }
601
590
 
602
- function hasActiveUsers(state: AuthStorageState, projectId: string): boolean {
603
- for (const user of state.users.values()) {
604
- if (user.projectId === projectId && user.status === "active") {
605
- return true
606
- }
607
- }
608
-
609
- return false
610
- }
611
-
612
591
  function assertSignInSessionAudience(
613
592
  projectId: string,
614
593
  sessionAudience: string,
@@ -4,7 +4,8 @@ import type { AuthorizablePrincipal } from "../../execution/types"
4
4
  export type UserStatus = "active" | "suspended"
5
5
  export type ServiceAccountStatus = "active" | "suspended"
6
6
  export type InvitationStatus = "pending" | "accepted" | "revoked"
7
- export type GroupMembershipSource = "invitation" | "manual" | "agent"
7
+ /** `directory`: owned by the identity provider, and synced to its groups on every sign-in. */
8
+ export type GroupMembershipSource = "invitation" | "manual" | "agent" | "directory"
8
9
  export type DeviceAuthorizationStatus = "pending" | "approved" | "denied" | "consumed"
9
10
  export const MAX_PENDING_DEVICE_AUTHORIZATIONS = 100
10
11
 
@@ -45,7 +46,8 @@ export interface ServiceAccountGroupMembershipRecord {
45
46
  readonly projectId: string
46
47
  readonly serviceAccountId: string
47
48
  readonly groupId: string
48
- readonly source: GroupMembershipSource
49
+ /** Service accounts never sign in, so no identity provider owns their memberships. */
50
+ readonly source: Exclude<GroupMembershipSource, "directory">
49
51
  readonly createdAt: Date
50
52
  }
51
53
 
@@ -269,7 +271,7 @@ export interface UpsertAuthServiceAccountGroupMembershipInput {
269
271
  readonly projectId: string
270
272
  readonly serviceAccountId: string
271
273
  readonly groupId: string
272
- readonly source: GroupMembershipSource
274
+ readonly source: ServiceAccountGroupMembershipRecord["source"]
273
275
  readonly createdAt?: Date
274
276
  }
275
277
 
@@ -431,7 +433,6 @@ export interface CompleteMagicLinkSignInInput {
431
433
  readonly newUserDisplayName?: string
432
434
  readonly newUserAvatarUrl?: string
433
435
  readonly allowUserCreationWithoutInvitation?: boolean
434
- readonly requireNoActiveUsersForUserCreation?: boolean
435
436
  readonly manualGroupIds?: readonly string[]
436
437
  readonly session: CompleteAuthSessionInput
437
438
  }
@@ -442,15 +443,22 @@ export interface CompleteOidcSignInInput {
442
443
  readonly stateHash: string
443
444
  readonly completedAt: Date
444
445
  readonly subject: string
446
+ /**
447
+ * An address the strategy has already decided to trust. Without an identity for `subject`, it
448
+ * links the user who has this email, or claims this email's invitation for a new user.
449
+ */
445
450
  readonly email: string
446
- readonly emailVerified?: boolean
447
451
  readonly displayName?: string
448
452
  readonly avatarUrl?: string
449
453
  readonly claims?: Readonly<Record<string, unknown>>
450
- readonly autoLinkByVerifiedEmail?: boolean
451
454
  readonly allowUserCreationWithoutInvitation?: boolean
452
- readonly requireNoActiveUsersForUserCreation?: boolean
453
455
  readonly manualGroupIds?: readonly string[]
456
+ /**
457
+ * The groups the identity provider grants this user. When present, the user's `directory`
458
+ * memberships become exactly these; memberships from any other source are never touched, and a
459
+ * group the user already has from another source keeps it. Omitted, nothing is synced.
460
+ */
461
+ readonly directoryGroupIds?: readonly string[]
454
462
  readonly newUserId: string
455
463
  readonly session: CompleteAuthSessionInput
456
464
  }