@tuturuuu/utils 0.13.0 → 0.15.1
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 +64 -0
- package/biome.json +1 -1
- package/package.json +10 -10
- package/src/__tests__/app-url.test.ts +4 -0
- package/src/calendar-settings-resolver.ts +200 -0
- package/src/internal-domains.ts +32 -0
- package/src/launchable-apps.test.ts +44 -0
- package/src/launchable-apps.ts +52 -39
- package/src/next-config.test.ts +92 -0
- package/src/next-config.ts +23 -0
- package/src/platform-release.test.ts +2 -2
- package/src/platform-release.ts +3 -1
- package/src/portless.ts +3 -0
- package/src/shared-cookie.test.ts +114 -0
- package/src/shared-cookie.ts +235 -0
- package/src/topic-announcements.ts +1 -0
- package/src/user-helper.ts +6 -95
- package/src/workspace-user-link.test.ts +53 -0
- package/src/workspace-user-link.ts +122 -0
- package/src/yjs-helper.ts +2 -2
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { TypedSupabaseClient } from '@tuturuuu/supabase/types';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
createAdminClient: vi.fn(),
|
|
6
|
+
createClient: vi.fn(),
|
|
7
|
+
verifyWorkspaceMembershipType: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('@tuturuuu/supabase/next/server', () => ({
|
|
11
|
+
createAdminClient: mocks.createAdminClient,
|
|
12
|
+
createClient: mocks.createClient,
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('./workspace-helper', () => ({
|
|
15
|
+
verifyWorkspaceMembershipType: mocks.verifyWorkspaceMembershipType,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { getWorkspaceUserLinkForUser } from './workspace-user-link';
|
|
19
|
+
|
|
20
|
+
describe('getWorkspaceUserLinkForUser', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('uses an injected authorization client for satellite actors', async () => {
|
|
26
|
+
const link = {
|
|
27
|
+
created_at: '2026-07-13T00:00:00.000Z',
|
|
28
|
+
platform_user_id: 'platform-user-1',
|
|
29
|
+
virtual_user_id: 'virtual-user-1',
|
|
30
|
+
ws_id: 'workspace-1',
|
|
31
|
+
};
|
|
32
|
+
const maybeSingle = vi.fn().mockResolvedValue({ data: link, error: null });
|
|
33
|
+
const limit = vi.fn(() => ({ maybeSingle }));
|
|
34
|
+
const secondEq = vi.fn(() => ({ limit }));
|
|
35
|
+
const firstEq = vi.fn(() => ({ eq: secondEq }));
|
|
36
|
+
const select = vi.fn(() => ({ eq: firstEq }));
|
|
37
|
+
const authorizationClient = {
|
|
38
|
+
from: vi.fn(() => ({ select })),
|
|
39
|
+
} as unknown as TypedSupabaseClient;
|
|
40
|
+
|
|
41
|
+
await expect(
|
|
42
|
+
getWorkspaceUserLinkForUser('workspace-1', 'platform-user-1', {
|
|
43
|
+
authorizationClient,
|
|
44
|
+
})
|
|
45
|
+
).resolves.toEqual(link);
|
|
46
|
+
|
|
47
|
+
expect(authorizationClient.from).toHaveBeenCalledWith(
|
|
48
|
+
'workspace_user_linked_users'
|
|
49
|
+
);
|
|
50
|
+
expect(mocks.createClient).not.toHaveBeenCalled();
|
|
51
|
+
expect(mocks.createAdminClient).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAdminClient,
|
|
3
|
+
createClient,
|
|
4
|
+
} from '@tuturuuu/supabase/next/server';
|
|
5
|
+
import type { TypedSupabaseClient } from '@tuturuuu/supabase/types';
|
|
6
|
+
import type { WorkspaceUser } from '@tuturuuu/types/primitives/WorkspaceUser';
|
|
7
|
+
import { resolveWorkspaceId } from './constants';
|
|
8
|
+
import { verifyWorkspaceMembershipType } from './workspace-helper';
|
|
9
|
+
|
|
10
|
+
export interface WorkspaceUserLink {
|
|
11
|
+
platform_user_id: string;
|
|
12
|
+
virtual_user_id: string;
|
|
13
|
+
ws_id: string;
|
|
14
|
+
created_at: string;
|
|
15
|
+
workspace_users?: WorkspaceUser;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const LINK_SELECT =
|
|
19
|
+
'platform_user_id, virtual_user_id, ws_id, created_at, workspace_users!virtual_user_id(*)';
|
|
20
|
+
|
|
21
|
+
type LinkRow = {
|
|
22
|
+
platform_user_id: string;
|
|
23
|
+
virtual_user_id: string;
|
|
24
|
+
ws_id: string;
|
|
25
|
+
created_at: string;
|
|
26
|
+
workspace_users?: unknown;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function toLink(row: LinkRow): WorkspaceUserLink {
|
|
30
|
+
const linkedData = row.workspace_users;
|
|
31
|
+
return {
|
|
32
|
+
platform_user_id: row.platform_user_id,
|
|
33
|
+
virtual_user_id: row.virtual_user_id,
|
|
34
|
+
ws_id: row.ws_id,
|
|
35
|
+
created_at: row.created_at,
|
|
36
|
+
...(linkedData ? { workspace_users: linkedData as WorkspaceUser } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the workspace-user link for an EXPLICIT platform user id.
|
|
42
|
+
*
|
|
43
|
+
* `getCurrentWorkspaceUser` (user-helper) resolves the actor from Supabase auth
|
|
44
|
+
* and then delegates here. Satellite apps must not do that: they resolve the
|
|
45
|
+
* actor from Tuturuuu app-session auth (`getSatelliteAppSessionUser`) and pass
|
|
46
|
+
* the id in, which is why this lives outside `user-helper` — the internal-app-auth
|
|
47
|
+
* guard forbids registered apps from importing that module.
|
|
48
|
+
*/
|
|
49
|
+
export async function getWorkspaceUserLinkForUser(
|
|
50
|
+
wsId: string,
|
|
51
|
+
userId: string,
|
|
52
|
+
options: {
|
|
53
|
+
authorizationClient?: TypedSupabaseClient;
|
|
54
|
+
autoRepair?: boolean;
|
|
55
|
+
} = {}
|
|
56
|
+
): Promise<WorkspaceUserLink | null> {
|
|
57
|
+
const { authorizationClient, autoRepair = true } = options;
|
|
58
|
+
if (!userId) return null;
|
|
59
|
+
|
|
60
|
+
const supabase = authorizationClient ?? (await createClient());
|
|
61
|
+
const resolvedWsId = resolveWorkspaceId(wsId);
|
|
62
|
+
|
|
63
|
+
const { data: workspaceUser } = await supabase
|
|
64
|
+
.from('workspace_user_linked_users')
|
|
65
|
+
.select(LINK_SELECT)
|
|
66
|
+
.eq('platform_user_id', userId)
|
|
67
|
+
.eq('ws_id', resolvedWsId)
|
|
68
|
+
.limit(1)
|
|
69
|
+
.maybeSingle();
|
|
70
|
+
|
|
71
|
+
if (workspaceUser) return toLink(workspaceUser as LinkRow);
|
|
72
|
+
|
|
73
|
+
if (!autoRepair) return null;
|
|
74
|
+
|
|
75
|
+
const membership = await verifyWorkspaceMembershipType({
|
|
76
|
+
wsId: resolvedWsId,
|
|
77
|
+
userId,
|
|
78
|
+
supabase,
|
|
79
|
+
requiredType: 'MEMBER',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!membership.ok) return null;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const sbAdmin = await createAdminClient();
|
|
86
|
+
// Note: ensure_workspace_user_link is defined in migration 20260112060000.
|
|
87
|
+
// IMPORTANT: must use .bind() to preserve the Supabase client's `this` context.
|
|
88
|
+
const rpc = sbAdmin.rpc.bind(sbAdmin) as unknown as (
|
|
89
|
+
fn: string,
|
|
90
|
+
args: Record<string, unknown>
|
|
91
|
+
) => Promise<{ error: Error | null }>;
|
|
92
|
+
const { error: repairError } = await rpc('ensure_workspace_user_link', {
|
|
93
|
+
target_user_id: userId,
|
|
94
|
+
target_ws_id: resolvedWsId,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (repairError) {
|
|
98
|
+
console.error(
|
|
99
|
+
'[getWorkspaceUserLinkForUser] Failed to auto-repair workspace user link:',
|
|
100
|
+
repairError
|
|
101
|
+
);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { data: repairedUser } = await supabase
|
|
106
|
+
.from('workspace_user_linked_users')
|
|
107
|
+
.select(LINK_SELECT)
|
|
108
|
+
.eq('platform_user_id', userId)
|
|
109
|
+
.eq('ws_id', resolvedWsId)
|
|
110
|
+
.limit(1)
|
|
111
|
+
.maybeSingle();
|
|
112
|
+
|
|
113
|
+
if (repairedUser) return toLink(repairedUser as LinkRow);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error(
|
|
116
|
+
'[getWorkspaceUserLinkForUser] Error during auto-repair:',
|
|
117
|
+
err
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
package/src/yjs-helper.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSONContent } from '@tiptap/react';
|
|
2
|
-
import {
|
|
2
|
+
import type { Schema } from 'prosemirror-model';
|
|
3
3
|
import * as Y from 'yjs';
|
|
4
4
|
|
|
5
5
|
const PROSEMIRROR_FRAGMENT_NAME = 'prosemirror';
|
|
@@ -179,7 +179,7 @@ function validateJsonContent(
|
|
|
179
179
|
return jsonContent;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
return
|
|
182
|
+
return schema.nodeFromJSON(jsonContent).toJSON() as JSONContent;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
export function convertJsonContentToYjsState(
|