@sixb/pg 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.
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAGV,YAAY,EACZ,aAAa,EACb,oBAAoB,EACrB,MAAM,oBAAoB,CAAA;AAoG3B,OAAO,KAAK,EAAE,GAAG,EAAa,MAAM,aAAa,CAAA;AAEjD,MAAM,WAAW,wBAAwB;IACvC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACrC;AAED,eAAO,MAAM,2BAA2B,wBAAwB,CAAA;AAEhE,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,wBAAwB,CAAC,CAE1F;AAED,wBAAgB,MAAM,CACpB,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAC/D,OAAO,GAAE,oBAAyB,GACjC,aAAa,CAAC,wBAAwB,CAAC,CAEzC;AAED,wBAAgB,8BAA8B,CAC5C,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,MAAM,GACjB,SAAS,eAAe,EAAE,CAQ5B;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;IACjB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAC,wBAAwB,CAAC,CAAA;CAC5D,GAAG,eAAe,CAqBlB;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMxF;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5E;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAKrD;AA6KD,eAAO,MAAM,yBAAyB,wCAqDpC,CAAA"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,KAAK,EAGV,YAAY,EACZ,aAAa,EACb,oBAAoB,EACrB,MAAM,oBAAoB,CAAA;AAuG3B,OAAO,KAAK,EAAE,GAAG,EAAa,MAAM,aAAa,CAAA;AAEjD,MAAM,WAAW,wBAAwB;IACvC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACrC;AAED,eAAO,MAAM,2BAA2B,wBAAwB,CAAA;AAEhE,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,aAAa,CAAC,wBAAwB,CAAC,CAE1F;AAED,wBAAgB,MAAM,CACpB,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,CAAC,OAAO,EAAE,wBAAwB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EAC/D,OAAO,GAAE,oBAAyB,GACjC,aAAa,CAAC,wBAAwB,CAAC,CAEzC;AAED,wBAAgB,8BAA8B,CAC5C,GAAG,EAAE,GAAG,EACR,UAAU,EAAE,MAAM,GACjB,SAAS,eAAe,EAAE,CAQ5B;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;IACjB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAC,wBAAwB,CAAC,CAAA;CAC5D,GAAG,eAAe,CAqBlB;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMxF;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5E;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAKrD;AA6KD,eAAO,MAAM,yBAAyB,wCAsDpC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixb/pg",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "PostgreSQL storage provider for Sixb",
5
5
  "keywords": [
6
6
  "sixb",
@@ -40,10 +40,10 @@
40
40
  "postgres": "^3.4.5"
41
41
  },
42
42
  "peerDependencies": {
43
- "@sixb/core": "0.1.13"
43
+ "@sixb/core": "0.1.14"
44
44
  },
45
45
  "devDependencies": {
46
- "@sixb/core": "0.1.13",
46
+ "@sixb/core": "0.1.14",
47
47
  "@types/bun": "latest",
48
48
  "typescript": "^5.7.0"
49
49
  },
@@ -7,6 +7,7 @@ import type {
7
7
  CompleteSignInResult,
8
8
  DeviceAuthorizationRecord,
9
9
  GroupMembershipRecord,
10
+ GroupMembershipSource,
10
11
  InvitationRecord,
11
12
  SessionRecord,
12
13
  SuspendUserAndRevokeSessionsInput,
@@ -232,27 +233,6 @@ export class PgAuthStorage implements AuthStorage {
232
233
  }
233
234
  }
234
235
 
235
- if (
236
- shouldCreateUser &&
237
- !activeInvitation &&
238
- input.allowUserCreationWithoutInvitation &&
239
- input.requireNoActiveUsersForUserCreation &&
240
- (await hasActiveUsers(tx, projectId))
241
- ) {
242
- await consumeMagicLink(tx, {
243
- projectId,
244
- id: input.magicLinkId,
245
- tokenHash: input.tokenHash,
246
- consumedAt: completedAt,
247
- })
248
- return {
249
- error: new AuthStorageError(
250
- "user_creation_not_allowed",
251
- `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`
252
- ),
253
- }
254
- }
255
-
256
236
  await validateCompleteSessionInput(tx, projectId, input.session)
257
237
  assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience)
258
238
 
@@ -294,8 +274,9 @@ export class PgAuthStorage implements AuthStorage {
294
274
  projectId,
295
275
  user,
296
276
  })
297
- const groupMemberships = await this.applyManualGroups(tx, {
277
+ const groupMemberships = await this.applyGroups(tx, {
298
278
  completedAt,
279
+ source: "manual",
299
280
  existing: invitation.groupMemberships,
300
281
  groupIds: manualGroupIds,
301
282
  projectId,
@@ -392,16 +373,6 @@ export class PgAuthStorage implements AuthStorage {
392
373
  }
393
374
  }
394
375
 
395
- if (!identity && userRow && (!input.autoLinkByVerifiedEmail || !input.emailVerified)) {
396
- await this.consumeOidcAttempt(input, completedAt, projectId, tx)
397
- return {
398
- error: new AuthStorageError(
399
- "email_link_not_allowed",
400
- `[Sixb] OIDC identity cannot auto-link to user '${userRow.id}' for project '${projectId}'.`
401
- ),
402
- }
403
- }
404
-
405
376
  if (userRow?.status === "suspended") {
406
377
  await this.consumeOidcAttempt(input, completedAt, projectId, tx)
407
378
  return {
@@ -412,16 +383,6 @@ export class PgAuthStorage implements AuthStorage {
412
383
  }
413
384
  }
414
385
 
415
- if (shouldCreateUser && !input.emailVerified) {
416
- await this.consumeOidcAttempt(input, completedAt, projectId, tx)
417
- return {
418
- error: new AuthStorageError(
419
- "user_creation_not_allowed",
420
- `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
421
- ),
422
- }
423
- }
424
-
425
386
  if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
426
387
  await this.consumeOidcAttempt(input, completedAt, projectId, tx)
427
388
  return {
@@ -432,22 +393,6 @@ export class PgAuthStorage implements AuthStorage {
432
393
  }
433
394
  }
434
395
 
435
- if (
436
- shouldCreateUser &&
437
- !activeInvitation &&
438
- input.allowUserCreationWithoutInvitation &&
439
- input.requireNoActiveUsersForUserCreation &&
440
- (await hasActiveUsers(tx, projectId))
441
- ) {
442
- await this.consumeOidcAttempt(input, completedAt, projectId, tx)
443
- return {
444
- error: new AuthStorageError(
445
- "user_creation_not_allowed",
446
- `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
447
- ),
448
- }
449
- }
450
-
451
396
  await validateCompleteSessionInput(tx, projectId, input.session)
452
397
  assertSignInSessionAudience(projectId, input.session.audience, attempt.audience)
453
398
 
@@ -497,10 +442,17 @@ export class PgAuthStorage implements AuthStorage {
497
442
  projectId,
498
443
  user,
499
444
  })
500
- const groupMemberships = await this.applyManualGroups(tx, {
445
+ const groupMemberships = await this.syncDirectoryGroups(tx, {
501
446
  completedAt,
502
- existing: invitation.groupMemberships,
503
- groupIds: manualGroupIds,
447
+ existing: await this.applyGroups(tx, {
448
+ completedAt,
449
+ source: "manual",
450
+ existing: invitation.groupMemberships,
451
+ groupIds: manualGroupIds,
452
+ projectId,
453
+ userId: user.id,
454
+ }),
455
+ groupIds: input.directoryGroupIds,
504
456
  projectId,
505
457
  userId: user.id,
506
458
  })
@@ -703,13 +655,14 @@ export class PgAuthStorage implements AuthStorage {
703
655
  return { invitation, groupMemberships }
704
656
  }
705
657
 
706
- private async applyManualGroups(
658
+ private async applyGroups(
707
659
  sql: SQLClient,
708
660
  input: {
709
661
  readonly completedAt: Date
710
662
  readonly existing: readonly GroupMembershipRecord[]
711
663
  readonly groupIds: readonly string[]
712
664
  readonly projectId: string
665
+ readonly source: GroupMembershipSource
713
666
  readonly userId: string
714
667
  }
715
668
  ): Promise<readonly GroupMembershipRecord[]> {
@@ -721,7 +674,7 @@ export class PgAuthStorage implements AuthStorage {
721
674
  projectId: input.projectId,
722
675
  userId: input.userId,
723
676
  groupId,
724
- source: "manual",
677
+ source: input.source,
725
678
  createdAt: input.completedAt,
726
679
  })
727
680
  )
@@ -729,6 +682,33 @@ export class PgAuthStorage implements AuthStorage {
729
682
 
730
683
  return groupMemberships
731
684
  }
685
+ // Directory memberships become exactly `groupIds`. A group the user already holds from another
686
+ // source keeps that source, and memberships from other sources are never removed.
687
+ private async syncDirectoryGroups(
688
+ sql: SQLClient,
689
+ input: {
690
+ readonly completedAt: Date
691
+ readonly existing: readonly GroupMembershipRecord[]
692
+ readonly groupIds?: readonly string[]
693
+ readonly projectId: string
694
+ readonly userId: string
695
+ }
696
+ ): Promise<readonly GroupMembershipRecord[]> {
697
+ if (!input.groupIds) {
698
+ return input.existing
699
+ }
700
+
701
+ const groupIds = normalizeGroupIds(input.groupIds)
702
+ await sql`
703
+ DELETE FROM auth_group_memberships
704
+ WHERE project_id = ${input.projectId}
705
+ AND user_id = ${input.userId}
706
+ AND source = 'directory'
707
+ ${groupIds.length > 0 ? sql`AND group_id NOT IN ${sql(groupIds)}` : sql``}
708
+ `
709
+
710
+ return this.applyGroups(sql, { ...input, groupIds, source: "directory" })
711
+ }
732
712
 
733
713
  private async createSignInSession(
734
714
  sql: SQLClient,
@@ -815,18 +795,6 @@ export class PgAuthStorage implements AuthStorage {
815
795
  }
816
796
  }
817
797
 
818
- async function hasActiveUsers(sql: SQLClient, projectId: string): Promise<boolean> {
819
- const rows = (await sql`
820
- SELECT 1 AS active
821
- FROM auth_users
822
- WHERE project_id = ${projectId}
823
- AND status = 'active'
824
- LIMIT 1
825
- `) as Array<{ readonly active: number }>
826
-
827
- return rows.length > 0
828
- }
829
-
830
798
  function assertSignInSessionAudience(
831
799
  projectId: string,
832
800
  sessionAudience: string,
@@ -58,7 +58,7 @@ export interface PgAuthServiceAccountGroupMembershipRow {
58
58
  readonly project_id: string
59
59
  readonly service_account_id: string
60
60
  readonly group_id: string
61
- readonly source: GroupMembershipSource
61
+ readonly source: ServiceAccountGroupMembershipRecord["source"]
62
62
  readonly created_at: PgDate
63
63
  }
64
64
 
@@ -0,0 +1,5 @@
1
+ -- A membership the identity provider owns is recorded with source 'directory' and synced to the
2
+ -- provider's groups on every sign-in. Service accounts never sign in, so their table is unchanged.
3
+ ALTER TABLE auth_group_memberships DROP CONSTRAINT auth_group_memberships_source_check;
4
+ ALTER TABLE auth_group_memberships ADD CONSTRAINT auth_group_memberships_source_check
5
+ CHECK (source IN ('invitation', 'manual', 'agent', 'directory'));
package/src/migrations.ts CHANGED
@@ -106,6 +106,9 @@ import ontologyCommitAttributionSql from "./migrations/047-ontology-commit-attri
106
106
  type: "text",
107
107
  }
108
108
  import nativeSessionsSql from "./migrations/049-native-sessions.sql" with { type: "text" }
109
+ import directoryGroupMembershipsSql from "./migrations/050-directory-group-memberships.sql" with {
110
+ type: "text",
111
+ }
109
112
  import type { SQL, SQLClient } from "./pg-client"
110
113
 
111
114
  export interface PostgresMigrationContext {
@@ -408,6 +411,7 @@ export const postgresStorageMigrations = defineMigrations<PostgresMigrationConte
408
411
  pgSql("047-ontology-commit-attribution", ontologyCommitAttributionSql),
409
412
  pgSql("048-file-upload-sessions", fileUploadSessionsSql),
410
413
  pgSql("049-native-sessions", nativeSessionsSql),
414
+ pgSql("050-directory-group-memberships", directoryGroupMembershipsSql),
411
415
  ],
412
416
  })
413
417