@tuturuuu/utils 0.19.0 → 0.21.0

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,56 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.21.0](https://github.com/tutur3u/platform/compare/utils-v0.20.0...utils-v0.21.0) (2026-07-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * **git:** add fast repository satellite ([51982ae](https://github.com/tutur3u/platform/commit/51982ae8618bb7463e30c97f6e731551ec673660))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **workspaces:** preserve creator administration access ([cea32ea](https://github.com/tutur3u/platform/commit/cea32ea22acaae6be6e39552be4af47e6f4dcdc7))
14
+
15
+ ## [0.20.0](https://github.com/tutur3u/platform/compare/utils-v0.19.0...utils-v0.20.0) (2026-07-27)
16
+
17
+
18
+ ### Features
19
+
20
+ * **ai:** add workspace AI Studio and legal coverage ([6de4e39](https://github.com/tutur3u/platform/commit/6de4e395cc5568f4943604ac667e3cebf324be13))
21
+ * **apps:** unify and redesign app catalog ([b679068](https://github.com/tutur3u/platform/commit/b679068339143a83b38610f363bec7f53ddc3706))
22
+ * **contacts:** migrate users attendance; add satellite-safe workspace-user link helper ([8f53453](https://github.com/tutur3u/platform/commit/8f534533793af8d25e7c2eca6e0147e459d78bbe))
23
+ * **contacts:** migrate users topic-announcements module ([01e19ab](https://github.com/tutur3u/platform/commit/01e19ab60461ccd94a0374bed74ce6e4d5d38a3b))
24
+ * **forms:** merge satellite migration ([e739f1b](https://github.com/tutur3u/platform/commit/e739f1bead568905458a42373ae24d13cd778907))
25
+ * **forms:** migrate product to satellite app ([51b9392](https://github.com/tutur3u/platform/commit/51b93928f1a12ebd4f4c753595fb33902ebfa66c))
26
+ * **infrastructure:** manage internal accounts ([02fd9f3](https://github.com/tutur3u/platform/commit/02fd9f3d1b4edb23c881e2dbb04bf244b36e6ed0))
27
+ * **inventory:** secure Square POS event checkout ([532b463](https://github.com/tutur3u/platform/commit/532b46372116ee0bebfd83ba2af762cc9f668c3c))
28
+ * **offline:** own service worker runtime and refresh dependencies ([ae44477](https://github.com/tutur3u/platform/commit/ae44477603c39f0513244514771653287338a89f))
29
+ * **reports:** add periodic reporting automation ([ec7bd5e](https://github.com/tutur3u/platform/commit/ec7bd5e10abb137e217d1dcf143624530276392f))
30
+ * **seo:** improve discovery metadata ([a574409](https://github.com/tutur3u/platform/commit/a57440984df45086c4989db07b02d402dd93c828))
31
+ * **seo:** improve discovery metadata ([8e2420d](https://github.com/tutur3u/platform/commit/8e2420d49e7b94c462d92222ea8f239ab92564f6))
32
+ * **seo:** improve page metadata ([0c621cb](https://github.com/tutur3u/platform/commit/0c621cb7fc1253b0a11ea21992503274052c6be0))
33
+ * **seo:** standardize app metadata ([6523d91](https://github.com/tutur3u/platform/commit/6523d91fedf38e19804d10ea3b82890db180bc6f))
34
+
35
+
36
+ ### Bug Fixes
37
+
38
+ * **chat:** harden personal Zalo integration ([f1d12c6](https://github.com/tutur3u/platform/commit/f1d12c60fe12ab3b01a1be3f0381573d44a32226))
39
+ * **ci:** keep task helper collection source-only ([7355a05](https://github.com/tutur3u/platform/commit/7355a05385ccd8d1e2d1bcf6ac15ae5297e057ad))
40
+ * **contacts:** restore group post mutations ([fe621e4](https://github.com/tutur3u/platform/commit/fe621e4d7e4126f705082cc3e815aafdeebe535a))
41
+ * **e2e:** run tasks lifecycle in CI ([bed2cd5](https://github.com/tutur3u/platform/commit/bed2cd590572748f65e75fcb15b7e4b714b9034c))
42
+ * **inventory:** harden sales analytics and access ([53968d3](https://github.com/tutur3u/platform/commit/53968d38a0c5425a3c1065ab2f645e30a0145c8f))
43
+ * **mobile:** preserve bearer auth across satellites ([f890170](https://github.com/tutur3u/platform/commit/f89017044cf3aaaa6a1b15c31c64a81d75cfdab2))
44
+ * resolve code quality findings ([63f10b5](https://github.com/tutur3u/platform/commit/63f10b5ec22a4194f48f448ee2b1b088b5da8f08))
45
+ * **security:** prevent clickjacking across apps ([c8ed36e](https://github.com/tutur3u/platform/commit/c8ed36e708c08ebe3b17ee7f6249513644108c48))
46
+ * **security:** remediate code scanning findings ([023db2e](https://github.com/tutur3u/platform/commit/023db2edf4b0557be108a9d772cbc7e2223af947))
47
+
48
+
49
+ ### Performance Improvements
50
+
51
+ * **ci:** enable repository-wide remote caching ([6250f91](https://github.com/tutur3u/platform/commit/6250f91d745ef987a4fc86c797aedf41542f421b))
52
+ * **storefront:** share customer navigation shell ([21cb78d](https://github.com/tutur3u/platform/commit/21cb78db484d1e1fdb9f2469fcac4e3a948bd176))
53
+
3
54
  ## [0.19.0](https://github.com/tutur3u/platform/compare/utils-v0.18.0...utils-v0.19.0) (2026-07-27)
4
55
 
5
56
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/utils",
3
3
  "license": "MIT",
4
- "version": "0.19.0",
4
+ "version": "0.21.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -24,7 +24,7 @@
24
24
  "@tiptap/react": "3.28.0",
25
25
  "@tuturuuu/google": "0.1.0",
26
26
  "@tuturuuu/icons": "0.1.0",
27
- "@tuturuuu/internal-api": "0.23.0",
27
+ "@tuturuuu/internal-api": "0.25.0",
28
28
  "@tuturuuu/supabase": "0.5.0",
29
29
  "@upstash/ratelimit": "^2.0.8",
30
30
  "@upstash/redis": "^1.38.0",
@@ -45,7 +45,7 @@
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "devDependencies": {
48
- "@tuturuuu/types": "0.22.0",
48
+ "@tuturuuu/types": "0.24.0",
49
49
  "@tuturuuu/typescript-config": "0.1.1",
50
50
  "@types/diff": "^8.0.0",
51
51
  "@types/node": "^26.1.1",
@@ -298,6 +298,7 @@ describe('Portless app origin registry', () => {
298
298
  'https://external.tuturuuu.localhost',
299
299
  'https://finance.tuturuuu.localhost',
300
300
  'https://forms.tuturuuu.localhost',
301
+ 'https://git.tuturuuu.localhost',
301
302
  'https://hive.tuturuuu.localhost',
302
303
  'https://realtime.hive.tuturuuu.localhost',
303
304
  'https://infra.tuturuuu.localhost',
@@ -32,6 +32,7 @@ describe('workspace permission catalog', () => {
32
32
  expect(permissionIds).not.toContain('view_infrastructure');
33
33
  expect(permissionIds).not.toContain('manage_infrastructure_stress_tests');
34
34
  expect(permissionIds).not.toContain('manage_internal_accounts');
35
+ expect(permissionIds).not.toContain('manage_git_repositories');
35
36
  expect(permissionIds).not.toContain('manage_workspace_secrets');
36
37
  });
37
38
 
@@ -42,6 +43,7 @@ describe('workspace permission catalog', () => {
42
43
  }).map((permission) => permission.id);
43
44
 
44
45
  expect(rootPermissionIds).toContain('manage_internal_accounts');
46
+ expect(rootPermissionIds).toContain('manage_git_repositories');
45
47
  });
46
48
 
47
49
  it('keeps internal account permission translations in shared role editors', () => {
@@ -75,6 +77,7 @@ describe('workspace permission catalog', () => {
75
77
  expect(permissionIds).toContain('view_infrastructure');
76
78
  expect(permissionIds).toContain('manage_infrastructure_stress_tests');
77
79
  expect(permissionIds).toContain('manage_internal_accounts');
80
+ expect(permissionIds).toContain('manage_git_repositories');
78
81
  expect(permissionIds).toContain('manage_external_migrations');
79
82
  expect(permissionIds).toContain('manage_workspace_secrets');
80
83
  expect(new Set(permissionIds).size).toBe(permissionIds.length);
@@ -560,24 +560,153 @@ describe('getPermissions membership type gate', () => {
560
560
  expect(adminAuthGetUser).not.toHaveBeenCalled();
561
561
  expect(personalWorkspaceQuery.eq).toHaveBeenCalledWith('personal', true);
562
562
  expect(personalWorkspaceQuery.eq).toHaveBeenCalledWith(
563
- 'workspace_members.user_id',
563
+ 'creator_id',
564
564
  'user-1'
565
565
  );
566
566
  expect(membershipQuery.eq).toHaveBeenCalledWith(
567
567
  'ws_id',
568
568
  personalWorkspaceId
569
569
  );
570
- expect(roleMembersQuery.eq).toHaveBeenCalledWith(
571
- 'workspace_roles.ws_id',
572
- personalWorkspaceId
573
- );
574
570
  expect(workspaceQuery.eq).toHaveBeenCalledWith('id', personalWorkspaceId);
575
- expect(defaultPermissionsQuery.eq).toHaveBeenCalledWith(
576
- 'ws_id',
577
- personalWorkspaceId
571
+ expect(roleMembersQuery.select).not.toHaveBeenCalled();
572
+ expect(defaultPermissionsQuery.select).not.toHaveBeenCalled();
573
+ });
574
+
575
+ it('grants full member permissions to a creator with a missing membership row', async () => {
576
+ const workspaceId = '11111111-1111-4111-8111-111111111111';
577
+ const membershipQuery = {
578
+ select: vi.fn(),
579
+ eq: vi.fn(),
580
+ maybeSingle: vi.fn(),
581
+ };
582
+ membershipQuery.select.mockReturnValue(membershipQuery);
583
+ membershipQuery.eq.mockReturnValue(membershipQuery);
584
+ membershipQuery.maybeSingle.mockResolvedValue({
585
+ data: null,
586
+ error: null,
587
+ });
588
+
589
+ const workspaceQuery = {
590
+ select: vi.fn(),
591
+ eq: vi.fn(),
592
+ single: vi.fn(),
593
+ };
594
+ workspaceQuery.select.mockReturnValue(workspaceQuery);
595
+ workspaceQuery.eq.mockReturnValue(workspaceQuery);
596
+ workspaceQuery.single.mockResolvedValue({
597
+ data: { creator_id: 'user-1' },
598
+ error: null,
599
+ });
600
+
601
+ mockCreateAdminClient.mockResolvedValue({
602
+ from: vi.fn((table: string) => {
603
+ if (table === 'workspace_members') return membershipQuery;
604
+ if (table === 'workspaces') return workspaceQuery;
605
+ throw new Error(`Unexpected admin table lookup: ${table}`);
606
+ }),
607
+ });
608
+
609
+ const permissions = await getPermissions({
610
+ user: { id: 'user-1', email: 'creator@example.com' },
611
+ wsId: workspaceId,
612
+ });
613
+
614
+ expect(permissions?.membershipType).toBe('MEMBER');
615
+ expect(permissions?.containsPermission('manage_workspace_settings')).toBe(
616
+ true
617
+ );
618
+ expect(permissions?.containsPermission('manage_workspace_members')).toBe(
619
+ true
620
+ );
621
+ });
622
+
623
+ it('normalizes a creator GUEST membership to effective MEMBER permissions', async () => {
624
+ const workspaceId = '11111111-1111-4111-8111-111111111111';
625
+ const membershipQuery = {
626
+ select: vi.fn(),
627
+ eq: vi.fn(),
628
+ maybeSingle: vi.fn(),
629
+ };
630
+ membershipQuery.select.mockReturnValue(membershipQuery);
631
+ membershipQuery.eq.mockReturnValue(membershipQuery);
632
+ membershipQuery.maybeSingle.mockResolvedValue({
633
+ data: { type: 'GUEST' },
634
+ error: null,
635
+ });
636
+
637
+ const workspaceQuery = {
638
+ select: vi.fn(),
639
+ eq: vi.fn(),
640
+ single: vi.fn(),
641
+ };
642
+ workspaceQuery.select.mockReturnValue(workspaceQuery);
643
+ workspaceQuery.eq.mockReturnValue(workspaceQuery);
644
+ workspaceQuery.single.mockResolvedValue({
645
+ data: { creator_id: 'user-1' },
646
+ error: null,
647
+ });
648
+
649
+ mockCreateAdminClient.mockResolvedValue({
650
+ from: vi.fn((table: string) => {
651
+ if (table === 'workspace_members') return membershipQuery;
652
+ if (table === 'workspaces') return workspaceQuery;
653
+ throw new Error(`Unexpected admin table lookup: ${table}`);
654
+ }),
655
+ });
656
+
657
+ const permissions = await getPermissions({
658
+ user: { id: 'user-1', email: 'creator@example.com' },
659
+ wsId: workspaceId,
660
+ });
661
+
662
+ expect(permissions?.membershipType).toBe('MEMBER');
663
+ expect(permissions?.containsPermission('manage_workspace_roles')).toBe(
664
+ true
578
665
  );
579
666
  });
580
667
 
668
+ it('denies an unrelated user when the membership row is missing', async () => {
669
+ const workspaceId = '11111111-1111-4111-8111-111111111111';
670
+ const membershipQuery = {
671
+ select: vi.fn(),
672
+ eq: vi.fn(),
673
+ maybeSingle: vi.fn(),
674
+ };
675
+ membershipQuery.select.mockReturnValue(membershipQuery);
676
+ membershipQuery.eq.mockReturnValue(membershipQuery);
677
+ membershipQuery.maybeSingle.mockResolvedValue({
678
+ data: null,
679
+ error: null,
680
+ });
681
+
682
+ const workspaceQuery = {
683
+ select: vi.fn(),
684
+ eq: vi.fn(),
685
+ single: vi.fn(),
686
+ };
687
+ workspaceQuery.select.mockReturnValue(workspaceQuery);
688
+ workspaceQuery.eq.mockReturnValue(workspaceQuery);
689
+ workspaceQuery.single.mockResolvedValue({
690
+ data: { creator_id: 'owner-1' },
691
+ error: null,
692
+ });
693
+
694
+ mockCreateAdminClient.mockResolvedValue({
695
+ from: vi.fn((table: string) => {
696
+ if (table === 'workspace_members') return membershipQuery;
697
+ if (table === 'workspaces') return workspaceQuery;
698
+ throw new Error(`Unexpected admin table lookup: ${table}`);
699
+ }),
700
+ });
701
+
702
+ const permissions = await getPermissions({
703
+ user: { id: 'user-1', email: 'unrelated@example.com' },
704
+ wsId: workspaceId,
705
+ });
706
+
707
+ expect(permissions).toBeNull();
708
+ });
709
+
581
710
  it('returns a read-only permission context for a guest without defaults', async () => {
582
711
  const membershipQuery = {
583
712
  select: vi.fn(),
@@ -116,6 +116,10 @@ export const PRODUCTION_INTERNAL_APP_DOMAINS = [
116
116
  name: 'forms',
117
117
  url: 'https://forms.tuturuuu.com',
118
118
  },
119
+ {
120
+ name: 'git',
121
+ url: 'https://git.tuturuuu.com',
122
+ },
119
123
  {
120
124
  name: 'hive',
121
125
  url: 'https://hive.tuturuuu.com',
@@ -231,6 +235,10 @@ export const PORTLESS_INTERNAL_APP_DOMAINS = [
231
235
  name: 'forms',
232
236
  url: getTuturuuuPortlessAppOrigin('forms'),
233
237
  },
238
+ {
239
+ name: 'git',
240
+ url: getTuturuuuPortlessAppOrigin('git'),
241
+ },
234
242
  {
235
243
  name: 'hive',
236
244
  url: getTuturuuuPortlessAppOrigin('hive'),
@@ -342,6 +350,10 @@ export const LOCALHOST_INTERNAL_APP_DOMAINS = [
342
350
  name: 'forms',
343
351
  url: 'http://localhost:7828',
344
352
  },
353
+ {
354
+ name: 'git',
355
+ url: 'http://localhost:7830',
356
+ },
345
357
  {
346
358
  name: 'hive',
347
359
  url: 'http://localhost:7814',
@@ -32,6 +32,7 @@ describe('launchable apps', () => {
32
32
  });
33
33
  expect(getLaunchableAppByTitle('Tuturuuu')?.slug).toBe('platform');
34
34
  expect(getLaunchableAppByTitle('questionnaires')?.slug).toBe('forms');
35
+ expect(getLaunchableAppByTitle('repositories')?.slug).toBe('git');
35
36
  expect(getLaunchableAppByTitle('apps gateway')?.slug).toBe('apps');
36
37
  });
37
38
 
@@ -45,6 +46,11 @@ describe('launchable apps', () => {
45
46
  expect(getLaunchableApp('external')).toBeNull();
46
47
  expect(getLaunchableApp('playground')).toBeNull();
47
48
  expect(getLaunchableApp('platform')?.category).toBe('productivity');
49
+ expect(getLaunchableApp('git')).toMatchObject({
50
+ defaultPath: '/tutur3u/platform',
51
+ localhostOrigin: 'http://localhost:7830',
52
+ productionUrl: 'https://git.tuturuuu.com',
53
+ });
48
54
  expect(getLaunchableApp('apps')?.category).toBe('miscellaneous');
49
55
  expect(getLaunchableApp('cms')?.category).toBe('operations');
50
56
  expect(getLaunchableApp('rewise')?.category).toBe('ai');
@@ -113,6 +113,18 @@ export const LAUNCHABLE_APPS = [
113
113
  title: 'Drive',
114
114
  workspacePathResolver: workspaceRootPath,
115
115
  },
116
+ {
117
+ aliases: ['Source', 'Repositories', 'GitHub', 'Code'],
118
+ appRoot: 'apps/git',
119
+ category: 'productivity',
120
+ defaultPath: '/tutur3u/platform',
121
+ localhostOrigin: 'http://localhost:7830',
122
+ packageName: '@tuturuuu/git',
123
+ portlessApp: 'git',
124
+ productionUrl: 'https://git.tuturuuu.com',
125
+ slug: 'git',
126
+ title: 'Git',
127
+ },
116
128
  {
117
129
  aliases: ['App Launcher', 'Gateway', 'Apps Gateway'],
118
130
  appRoot: 'apps/apps',
@@ -19,6 +19,7 @@ import {
19
19
  FileKey2,
20
20
  FileText,
21
21
  FlaskConical,
22
+ GitBranch,
22
23
  HandCoins,
23
24
  HardDrive,
24
25
  House,
@@ -131,6 +132,16 @@ export const permissionGroups = ({
131
132
  disableOnProduction: false,
132
133
  disabled: false,
133
134
  },
135
+ {
136
+ id: 'manage_git_repositories' as PermissionId,
137
+ icon: <GitBranch />,
138
+ title: t('ws-roles.manage_git_repositories'),
139
+ description: t(
140
+ 'ws-roles.manage_git_repositories_description'
141
+ ),
142
+ disableOnProduction: false,
143
+ disabled: false,
144
+ },
134
145
  {
135
146
  id: 'manage_mobile_deployment_vault' as PermissionId,
136
147
  icon: <FileKey2 />,
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  describe('platform release metadata', () => {
9
9
  it('uses the centralized shared browser app version', () => {
10
- expect(TUTURUUU_PLATFORM_VERSION).toBe('0.28.0'); // x-release-please-version
10
+ expect(TUTURUUU_PLATFORM_VERSION).toBe('0.30.0'); // x-release-please-version
11
11
  });
12
12
 
13
13
  it('preserves the source timestamp in builtAt and derives a short hash', () => {
@@ -1,6 +1,6 @@
1
1
  import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
2
2
 
3
- export const TUTURUUU_PLATFORM_VERSION = '0.28.0'; // x-release-please-version
3
+ export const TUTURUUU_PLATFORM_VERSION = '0.30.0'; // x-release-please-version
4
4
 
5
5
  export type PlatformBuildMetadataInput = {
6
6
  /** ISO timestamp of the source commit used for the build, not deployment time. */
package/src/portless.ts CHANGED
@@ -17,6 +17,7 @@ export const TUTURUUU_PORTLESS_APP_ORIGINS = {
17
17
  external: `https://external.${TUTURUUU_PORTLESS_ROOT_HOST}`,
18
18
  finance: `https://finance.${TUTURUUU_PORTLESS_ROOT_HOST}`,
19
19
  forms: `https://forms.${TUTURUUU_PORTLESS_ROOT_HOST}`,
20
+ git: `https://git.${TUTURUUU_PORTLESS_ROOT_HOST}`,
20
21
  hive: `https://hive.${TUTURUUU_PORTLESS_ROOT_HOST}`,
21
22
  'hive-realtime': `https://realtime.hive.${TUTURUUU_PORTLESS_ROOT_HOST}`,
22
23
  infra: `https://infra.${TUTURUUU_PORTLESS_ROOT_HOST}`,
@@ -689,19 +689,32 @@ async function resolveWorkspaceIdForPermissions({
689
689
  return normalizeWorkspaceId(wsId, authorizationClient);
690
690
  }
691
691
 
692
- const { data: workspace, error } = await authorizationClient
693
- .from('workspaces')
694
- .select('id, workspace_members!inner(user_id, type)')
695
- .eq('personal', true)
696
- .eq('workspace_members.user_id', principal.id)
697
- .eq('workspace_members.type', 'MEMBER')
698
- .maybeSingle();
692
+ const { data: creatorWorkspace, error: creatorWorkspaceError } =
693
+ await authorizationClient
694
+ .from('workspaces')
695
+ .select('id')
696
+ .eq('personal', true)
697
+ .eq('creator_id', principal.id)
698
+ .maybeSingle();
699
+
700
+ if (!creatorWorkspaceError && creatorWorkspace?.id) {
701
+ return creatorWorkspace.id;
702
+ }
699
703
 
700
- if (error || !workspace?.id) {
704
+ const { data: memberWorkspace, error: memberWorkspaceError } =
705
+ await authorizationClient
706
+ .from('workspaces')
707
+ .select('id, workspace_members!inner(user_id, type)')
708
+ .eq('personal', true)
709
+ .eq('workspace_members.user_id', principal.id)
710
+ .eq('workspace_members.type', 'MEMBER')
711
+ .maybeSingle();
712
+
713
+ if (memberWorkspaceError || !memberWorkspace?.id) {
701
714
  throw new Error('Personal workspace not found');
702
715
  }
703
716
 
704
- return workspace.id;
717
+ return memberWorkspace.id;
705
718
  }
706
719
 
707
720
  /**
@@ -761,7 +774,7 @@ async function getPermissionsImpl({
761
774
  let resolvedWorkspaceId: string;
762
775
  try {
763
776
  resolvedWorkspaceId = await resolveWorkspaceIdForPermissions({
764
- authorizationClient: authorizationClient as TypedSupabaseClient,
777
+ authorizationClient: sbAdmin,
765
778
  principal,
766
779
  wsId,
767
780
  });
@@ -769,21 +782,39 @@ async function getPermissionsImpl({
769
782
  return null;
770
783
  }
771
784
 
772
- const membership = await verifyWorkspaceMembershipType({
773
- wsId: resolvedWorkspaceId,
774
- userId,
775
- supabase: authorizationClient as TypedSupabaseClient,
776
- requiredType: 'ANY',
777
- });
785
+ const [membership, workspaceRes] = await Promise.all([
786
+ verifyWorkspaceMembershipType({
787
+ wsId: resolvedWorkspaceId,
788
+ userId,
789
+ supabase: authorizationClient as TypedSupabaseClient,
790
+ requiredType: 'ANY',
791
+ }),
792
+ sbAdmin
793
+ .from('workspaces')
794
+ .select('creator_id')
795
+ .eq('id', resolvedWorkspaceId)
796
+ .single(),
797
+ ]);
778
798
 
779
- if (!membership.ok) {
799
+ const { data: workspaceData, error: workspaceError } = workspaceRes;
800
+
801
+ if (workspaceError || !workspaceData) {
802
+ console.info('Workspace not found in getPermissions', resolvedWorkspaceId);
780
803
  return null;
781
804
  }
782
805
 
783
- const membershipType = membership.membershipType ?? 'MEMBER';
806
+ const isCreator = workspaceData.creator_id === userId;
807
+
808
+ if (!isCreator && !membership.ok) {
809
+ return null;
810
+ }
811
+
812
+ const membershipType = isCreator
813
+ ? 'MEMBER'
814
+ : (membership.membershipType ?? 'MEMBER');
784
815
 
785
816
  const permissionsQuery =
786
- membershipType === 'MEMBER'
817
+ membershipType === 'MEMBER' && !isCreator
787
818
  ? sbAdmin
788
819
  .from('workspace_role_members')
789
820
  .select(
@@ -794,40 +825,26 @@ async function getPermissionsImpl({
794
825
  .eq('workspace_roles.workspace_role_permissions.enabled', true)
795
826
  : Promise.resolve({ data: [], error: null });
796
827
 
797
- const workspaceQuery = sbAdmin
798
- .from('workspaces')
799
- .select('creator_id')
800
- .eq('id', resolvedWorkspaceId)
801
- .single();
828
+ const defaultQuery = isCreator
829
+ ? Promise.resolve({ data: [], error: null })
830
+ : sbAdmin
831
+ .from('workspace_default_permissions')
832
+ .select('permission')
833
+ .eq('ws_id', resolvedWorkspaceId)
834
+ .eq('member_type', membershipType)
835
+ .eq('enabled', true);
802
836
 
803
- const defaultQuery = sbAdmin
804
- .from('workspace_default_permissions')
805
- .select('permission')
806
- .eq('ws_id', resolvedWorkspaceId)
807
- .eq('member_type', membershipType)
808
- .eq('enabled', true);
809
-
810
- const [permissionsRes, workspaceRes, defaultRes] = await Promise.all([
837
+ const [permissionsRes, defaultRes] = await Promise.all([
811
838
  permissionsQuery,
812
- workspaceQuery,
813
839
  defaultQuery,
814
840
  ]);
815
841
 
816
842
  const { data: permissionsData, error: permissionsError } = permissionsRes;
817
- const { data: workspaceData, error: workspaceError } = workspaceRes;
818
843
  const { data: defaultData, error: defaultError } = defaultRes;
819
844
 
820
- if (!workspaceData) {
821
- console.info('Workspace not found in getPermissions', resolvedWorkspaceId);
822
- return null;
823
- }
824
-
825
845
  if (permissionsError) return null;
826
- if (workspaceError) return null;
827
846
  if (defaultError) return null;
828
847
 
829
- const isCreator =
830
- membershipType === 'MEMBER' && workspaceData.creator_id === userId;
831
848
  // if (DEV_MODE) {
832
849
  // console.log('--------------------');
833
850
  // console.log('Is creator', isCreator);