@tuturuuu/utils 0.27.0 → 0.28.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 +25 -0
- package/biome.json +1 -1
- package/package.json +16 -17
- package/src/__tests__/app-url.test.ts +1 -0
- package/src/__tests__/format.test.ts +43 -0
- package/src/__tests__/workspace-helper.test.ts +38 -0
- package/src/ai-temp-auth-constants.ts +2 -0
- package/src/ai-temp-auth.ts +2 -1
- package/src/calendar-utils.ts +10 -1
- package/src/format.ts +1 -6
- package/src/internal-domains.ts +3 -0
- package/src/keyboard-shortcuts.ts +40 -0
- package/src/launchable-apps.ts +12 -0
- package/src/meet-creation-policy.test.ts +21 -0
- package/src/meet-creation-policy.ts +6 -0
- package/src/onboarding.ts +1 -0
- package/src/platform-release.test.ts +1 -1
- package/src/platform-release.ts +1 -1
- package/src/portless.ts +1 -0
- package/src/workspace-helper.ts +20 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.28.0](https://github.com/tutur3u/platform/compare/utils-v0.27.1...utils-v0.28.0) (2026-09-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **calendar:** add installable Tasks and Calendar with bounded caching ([#5231](https://github.com/tutur3u/platform/issues/5231)) ([7078824](https://github.com/tutur3u/platform/commit/70788244874d811da3e1ce3a9af2c9389739a8c3))
|
|
9
|
+
* **colab:** launch multiplayer prompt workshops on Cloudflare ([7a4a3f9](https://github.com/tutur3u/platform/commit/7a4a3f9b4a12a0d7439cdb296398a21c0cd8939f))
|
|
10
|
+
* **meet:** host on Cloudflare and restrict meeting creation ([#5223](https://github.com/tutur3u/platform/issues/5223)) ([dff093b](https://github.com/tutur3u/platform/commit/dff093b4b6ade986ffd54f3c756d08589e47ad8e))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **calendar:** harden PWA lifecycle and cache retention ([14565c0](https://github.com/tutur3u/platform/commit/14565c0106e74b947e152fd8bb787de7f70bc54d))
|
|
16
|
+
* **calendar:** recover failed syncs and redesign calendar views ([7d22948](https://github.com/tutur3u/platform/commit/7d22948095a29d236f698d42dde362fc38311dd0))
|
|
17
|
+
* **calendar:** recover failed syncs and redesign calendar views ([#5230](https://github.com/tutur3u/platform/issues/5230)) ([e52605d](https://github.com/tutur3u/platform/commit/e52605d671f921b1564746afcdedf2478deee78e))
|
|
18
|
+
* **meet:** restrict meeting hosts and restore reliable call media ([2a8a533](https://github.com/tutur3u/platform/commit/2a8a53355181fc8d429653506f7c845c02cf7ae9))
|
|
19
|
+
|
|
20
|
+
## [0.27.1](https://github.com/tutur3u/platform/compare/utils-v0.27.0...utils-v0.27.1) (2026-09-04)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* **ai:** type workspace resolution failures ([10b9141](https://github.com/tutur3u/platform/commit/10b9141999894f25f7c4e0ca81a9d7e75dbfb2c1))
|
|
26
|
+
* **rewise:** bind AI work to selected workspace ([#5208](https://github.com/tutur3u/platform/issues/5208)) ([5f1d8f3](https://github.com/tutur3u/platform/commit/5f1d8f3544e87e78b6555cd679c36f2878d0dab9))
|
|
27
|
+
|
|
3
28
|
## [0.27.0](https://github.com/tutur3u/platform/compare/utils-v0.26.0...utils-v0.27.0) (2026-08-27)
|
|
4
29
|
|
|
5
30
|
|
package/biome.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/utils",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.28.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tutur3u/platform",
|
|
@@ -17,43 +17,42 @@
|
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@dnd-kit/core": "^6.3.1",
|
|
20
|
-
"@noble/ciphers": "^2.
|
|
21
|
-
"@noble/hashes": "^2.
|
|
22
|
-
"@tanstack/react-query": "^5.102.
|
|
23
|
-
"@tiptap/core": "3.
|
|
24
|
-
"@tiptap/react": "3.
|
|
20
|
+
"@noble/ciphers": "^2.4.0",
|
|
21
|
+
"@noble/hashes": "^2.4.0",
|
|
22
|
+
"@tanstack/react-query": "^5.102.8",
|
|
23
|
+
"@tiptap/core": "3.31.2",
|
|
24
|
+
"@tiptap/react": "3.31.2",
|
|
25
25
|
"@tuturuuu/google": "0.1.0",
|
|
26
26
|
"@tuturuuu/icons": "0.1.0",
|
|
27
|
-
"@tuturuuu/internal-api": "0.
|
|
27
|
+
"@tuturuuu/internal-api": "0.34.0",
|
|
28
28
|
"@tuturuuu/supabase": "0.5.1",
|
|
29
29
|
"@upstash/ratelimit": "^2.0.8",
|
|
30
|
-
"@upstash/redis": "^1.38.
|
|
31
|
-
"
|
|
30
|
+
"@upstash/redis": "^1.38.3",
|
|
31
|
+
"cn": "^0.2.5",
|
|
32
32
|
"date-fns": "^4.4.0",
|
|
33
33
|
"dayjs": "^1.11.23",
|
|
34
34
|
"diff": "^9.0.0",
|
|
35
35
|
"effect": "^3.22.1",
|
|
36
36
|
"moment": "^2.30.1",
|
|
37
|
-
"next": "^16.3.
|
|
37
|
+
"next": "^16.3.4",
|
|
38
38
|
"prosemirror-model": "^1.25.11",
|
|
39
39
|
"react": "^19.2.8",
|
|
40
40
|
"react-dom": "^19.2.8",
|
|
41
|
-
"tailwind-merge": "^3.6.0",
|
|
42
41
|
"uuid": "^14.0.2",
|
|
43
42
|
"y-prosemirror": "^1.3.7",
|
|
44
43
|
"yjs": "^13.6.32",
|
|
45
|
-
"zod": "^4.4
|
|
44
|
+
"zod": "^4.5.4"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
|
-
"@tuturuuu/types": "0.
|
|
47
|
+
"@tuturuuu/types": "0.29.0",
|
|
49
48
|
"@tuturuuu/typescript-config": "0.1.2",
|
|
50
49
|
"@types/diff": "^8.0.0",
|
|
51
|
-
"@types/node": "^26.
|
|
50
|
+
"@types/node": "^26.4.1",
|
|
52
51
|
"@types/react": "^19.2.18",
|
|
53
|
-
"@vitejs/plugin-react": "^6.1.
|
|
52
|
+
"@vitejs/plugin-react": "^6.1.1",
|
|
54
53
|
"typescript": "7.0.2",
|
|
55
54
|
"vite": "^8.2.2",
|
|
56
|
-
"vitest": "^
|
|
55
|
+
"vitest": "^5.0.0"
|
|
57
56
|
},
|
|
58
57
|
"exports": {
|
|
59
58
|
"./timezones": "./src/timezones.json",
|
|
@@ -77,5 +76,5 @@
|
|
|
77
76
|
"./encryption": "./src/encryption/index.ts",
|
|
78
77
|
"./finance": "./src/finance/index.ts"
|
|
79
78
|
},
|
|
80
|
-
"packageManager": "bun@1.4.
|
|
79
|
+
"packageManager": "bun@1.4.1"
|
|
81
80
|
}
|
|
@@ -292,6 +292,7 @@ describe('Portless app origin registry', () => {
|
|
|
292
292
|
'https://calendar.tuturuuu.localhost',
|
|
293
293
|
'https://chat.tuturuuu.localhost',
|
|
294
294
|
'https://cms.tuturuuu.localhost',
|
|
295
|
+
'https://colab.tuturuuu.localhost',
|
|
295
296
|
'https://contacts.tuturuuu.localhost',
|
|
296
297
|
'https://docs.tuturuuu.localhost',
|
|
297
298
|
'https://drive.tuturuuu.localhost',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
capitalize,
|
|
4
|
+
cn,
|
|
4
5
|
formatBytes,
|
|
5
6
|
formatCurrency,
|
|
6
7
|
formatDuration,
|
|
@@ -10,6 +11,48 @@ import {
|
|
|
10
11
|
} from '../format';
|
|
11
12
|
|
|
12
13
|
describe('Format Utilities', () => {
|
|
14
|
+
describe('cn', () => {
|
|
15
|
+
it('joins conditional class values with clsx-compatible semantics', () => {
|
|
16
|
+
expect(
|
|
17
|
+
cn('flex', false, null, undefined, [
|
|
18
|
+
'items-center',
|
|
19
|
+
{ 'opacity-50': true, hidden: false },
|
|
20
|
+
])
|
|
21
|
+
).toBe('flex items-center opacity-50');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('resolves conflicting Tailwind utilities using the last value', () => {
|
|
25
|
+
expect(cn('px-2 py-1', 'px-4', 'py-3')).toBe('px-4 py-3');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps independent responsive and state variants', () => {
|
|
29
|
+
expect(
|
|
30
|
+
cn(
|
|
31
|
+
'text-sm hover:text-base md:text-lg',
|
|
32
|
+
'text-xl hover:text-2xl md:text-3xl'
|
|
33
|
+
)
|
|
34
|
+
).toBe('text-xl hover:text-2xl md:text-3xl');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('preserves non-conflicting variants from separate inputs', () => {
|
|
38
|
+
expect(cn('text-sm hover:text-base', 'md:text-lg')).toBe(
|
|
39
|
+
'text-sm hover:text-base md:text-lg'
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('resolves arbitrary values and important modifiers', () => {
|
|
44
|
+
expect(cn('w-[10px] w-[var(--panel-width)]', '!p-4 p-2')).toBe(
|
|
45
|
+
'w-[var(--panel-width)] !p-4 p-2'
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('preserves non-Tailwind application classes', () => {
|
|
50
|
+
expect(cn('app-shell', 'feature-card', { selected: true })).toBe(
|
|
51
|
+
'app-shell feature-card selected'
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
13
56
|
describe('capitalize', () => {
|
|
14
57
|
it('capitalizes first letter of a word', () => {
|
|
15
58
|
expect(capitalize('hello')).toBe('Hello');
|
|
@@ -17,6 +17,9 @@ import {
|
|
|
17
17
|
isPersonalWorkspace,
|
|
18
18
|
isWorkspaceUuidLiteral,
|
|
19
19
|
normalizeWorkspaceId,
|
|
20
|
+
WorkspaceAuthError,
|
|
21
|
+
WorkspaceNotFoundError,
|
|
22
|
+
WorkspaceResolutionError,
|
|
20
23
|
} from '../workspace-helper';
|
|
21
24
|
|
|
22
25
|
describe('isWorkspaceUuidLiteral', () => {
|
|
@@ -410,6 +413,41 @@ describe('normalizeWorkspaceId', () => {
|
|
|
410
413
|
expect(query.eq).toHaveBeenCalledWith('workspace_members.type', 'MEMBER');
|
|
411
414
|
});
|
|
412
415
|
|
|
416
|
+
it('uses a typed error when personal workspace authentication is missing', async () => {
|
|
417
|
+
mockCreateClient.mockResolvedValue({
|
|
418
|
+
auth: { getUser: vi.fn().mockResolvedValue({ data: { user: null } }) },
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await expect(normalizeWorkspaceId('personal')).rejects.toBeInstanceOf(
|
|
422
|
+
WorkspaceAuthError
|
|
423
|
+
);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it.each([
|
|
427
|
+
[new Error('database unavailable'), WorkspaceResolutionError],
|
|
428
|
+
[null, WorkspaceNotFoundError],
|
|
429
|
+
])('distinguishes personal lookup failures', async (error, ErrorType) => {
|
|
430
|
+
const query = {
|
|
431
|
+
eq: vi.fn(),
|
|
432
|
+
maybeSingle: vi.fn().mockResolvedValue({ data: null, error }),
|
|
433
|
+
select: vi.fn(),
|
|
434
|
+
};
|
|
435
|
+
query.select.mockReturnValue(query);
|
|
436
|
+
query.eq.mockReturnValue(query);
|
|
437
|
+
mockCreateClient.mockResolvedValue({
|
|
438
|
+
auth: {
|
|
439
|
+
getUser: vi.fn().mockResolvedValue({
|
|
440
|
+
data: { user: { id: 'user-1' } },
|
|
441
|
+
}),
|
|
442
|
+
},
|
|
443
|
+
from: vi.fn(() => query),
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
await expect(normalizeWorkspaceId('personal')).rejects.toBeInstanceOf(
|
|
447
|
+
ErrorType
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
|
|
413
451
|
it('resolves handle via admin fallback when request-scoped lookup cannot see workspace', async () => {
|
|
414
452
|
const requestScopedQuery = {
|
|
415
453
|
select: vi.fn(),
|
package/src/ai-temp-auth.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { AI_TEMP_AUTH_HEADER } from './ai-temp-auth-constants';
|
|
2
3
|
import { getUpstashRestRedisClient } from './upstash-rest';
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
+
export { AI_TEMP_AUTH_HEADER } from './ai-temp-auth-constants';
|
|
5
6
|
export const AI_TEMP_AUTH_TTL_SECONDS = 60;
|
|
6
7
|
export const AI_CREDIT_SNAPSHOT_TTL_SECONDS = 15;
|
|
7
8
|
|
package/src/calendar-utils.ts
CHANGED
|
@@ -15,7 +15,16 @@ export function isAllDayEvent(
|
|
|
15
15
|
const durationMs = end.diff(start, 'millisecond');
|
|
16
16
|
const isMultipleOf24Hours = durationMs % (24 * 60 * 60 * 1000) === 0;
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const spansLocalMidnights =
|
|
19
|
+
start.startOf('day').valueOf() === start.valueOf() &&
|
|
20
|
+
end.startOf('day').valueOf() === end.valueOf();
|
|
21
|
+
const wallDuration =
|
|
22
|
+
durationMs + (end.utcOffset() - start.utcOffset()) * 60_000;
|
|
23
|
+
return (
|
|
24
|
+
durationMs > 0 &&
|
|
25
|
+
(isMultipleOf24Hours ||
|
|
26
|
+
(spansLocalMidnights && wallDuration % (24 * 60 * 60 * 1000) === 0))
|
|
27
|
+
);
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
// Helper function to convert Google Calendar all-day events to proper timezone
|
package/src/format.ts
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import { twMerge } from 'tailwind-merge';
|
|
1
|
+
export { cn } from 'cn';
|
|
3
2
|
|
|
4
3
|
import { getCurrencyLocale as getCurrencyLocaleFromConfig } from './currencies';
|
|
5
4
|
|
|
6
|
-
export function cn(...inputs: ClassValue[]): string {
|
|
7
|
-
return twMerge(clsx(inputs));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
5
|
export const capitalize = (s?: string | null): string => {
|
|
11
6
|
if (!s) return '';
|
|
12
7
|
if (s.length === 0) return s;
|
package/src/internal-domains.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from './portless';
|
|
5
5
|
|
|
6
6
|
export const PRODUCTION_INTERNAL_APP_DOMAINS = [
|
|
7
|
+
{ name: 'colab', url: 'https://colab.tuturuuu.com' },
|
|
7
8
|
{
|
|
8
9
|
name: 'ai',
|
|
9
10
|
url: 'https://ai.tuturuuu.com',
|
|
@@ -131,6 +132,7 @@ export const PRODUCTION_INTERNAL_APP_DOMAINS = [
|
|
|
131
132
|
] as const;
|
|
132
133
|
|
|
133
134
|
export const PORTLESS_INTERNAL_APP_DOMAINS = [
|
|
135
|
+
{ name: 'colab', url: getTuturuuuPortlessAppOrigin('colab') },
|
|
134
136
|
{
|
|
135
137
|
name: 'ai',
|
|
136
138
|
url: getTuturuuuPortlessAppOrigin('ai'),
|
|
@@ -250,6 +252,7 @@ export const PORTLESS_INTERNAL_APP_DOMAINS = [
|
|
|
250
252
|
] as const;
|
|
251
253
|
|
|
252
254
|
export const LOCALHOST_INTERNAL_APP_DOMAINS = [
|
|
255
|
+
{ name: 'colab', url: 'http://localhost:8795' },
|
|
253
256
|
{
|
|
254
257
|
name: 'ai',
|
|
255
258
|
url: 'http://localhost:7829',
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Shared guards for document/window keyboard commands. */
|
|
2
|
+
export function isEditableShortcutTarget(target: EventTarget | null): boolean {
|
|
3
|
+
return (
|
|
4
|
+
typeof Element !== 'undefined' &&
|
|
5
|
+
target instanceof Element &&
|
|
6
|
+
Boolean(
|
|
7
|
+
target.closest(
|
|
8
|
+
'input, textarea, select, [contenteditable]:not([contenteditable="false"]), [role="textbox"], [role="combobox"]'
|
|
9
|
+
)
|
|
10
|
+
)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isShortcutEventIgnored(
|
|
15
|
+
event: Pick<KeyboardEvent, 'defaultPrevented' | 'isComposing' | 'repeat'>
|
|
16
|
+
): boolean {
|
|
17
|
+
return event.defaultPrevented || event.isComposing || event.repeat;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isPageShortcutBlocked(event: KeyboardEvent): boolean {
|
|
21
|
+
if (isShortcutEventIgnored(event) || isEditableShortcutTarget(event.target))
|
|
22
|
+
return true;
|
|
23
|
+
const target = event.target;
|
|
24
|
+
if (
|
|
25
|
+
typeof Element !== 'undefined' &&
|
|
26
|
+
target instanceof Element &&
|
|
27
|
+
target.closest(
|
|
28
|
+
'[role="menu"], [role="listbox"], [role="dialog"], [role="alertdialog"]'
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
return true;
|
|
32
|
+
return (
|
|
33
|
+
typeof document !== 'undefined' &&
|
|
34
|
+
Boolean(
|
|
35
|
+
document.querySelector(
|
|
36
|
+
'[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"]'
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
}
|
package/src/launchable-apps.ts
CHANGED
|
@@ -387,6 +387,18 @@ export const LAUNCHABLE_APPS = [
|
|
|
387
387
|
title: 'Track',
|
|
388
388
|
workspacePathResolver: workspaceRootPath,
|
|
389
389
|
},
|
|
390
|
+
{
|
|
391
|
+
aliases: ['Prompt engineering', 'Team workshops', 'Multiplayer learning'],
|
|
392
|
+
appRoot: 'apps/colab',
|
|
393
|
+
category: 'learning',
|
|
394
|
+
defaultPath: '/',
|
|
395
|
+
localhostOrigin: 'http://localhost:8795',
|
|
396
|
+
packageName: '@tuturuuu/colab',
|
|
397
|
+
portlessApp: 'colab',
|
|
398
|
+
productionUrl: 'https://colab.tuturuuu.com',
|
|
399
|
+
slug: 'colab',
|
|
400
|
+
title: 'Colab',
|
|
401
|
+
},
|
|
390
402
|
] as const satisfies readonly LaunchableApp[];
|
|
391
403
|
|
|
392
404
|
export type LaunchableAppSlug = (typeof LAUNCHABLE_APPS)[number]['slug'];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { canCreateOnlineMeeting } from './meet-creation-policy';
|
|
3
|
+
|
|
4
|
+
describe('online meeting creator domain', () => {
|
|
5
|
+
it.each(['host@tuturuuu.com', 'Host@TUTURUUU.COM'])('allows %s', (email) => {
|
|
6
|
+
expect(canCreateOnlineMeeting(email)).toBe(true);
|
|
7
|
+
});
|
|
8
|
+
it.each([
|
|
9
|
+
null,
|
|
10
|
+
undefined,
|
|
11
|
+
'',
|
|
12
|
+
'@tuturuuu.com',
|
|
13
|
+
'host@gmail.com',
|
|
14
|
+
'host@sub.tuturuuu.com',
|
|
15
|
+
'host@tuturuuu.com.evil.test',
|
|
16
|
+
'host@evil.test@tuturuuu.com',
|
|
17
|
+
' host@tuturuuu.com',
|
|
18
|
+
])('rejects %s', (email) => {
|
|
19
|
+
expect(canCreateOnlineMeeting(email)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
package/src/onboarding.ts
CHANGED
|
@@ -71,6 +71,7 @@ export const GOAL_RECOMMENDATIONS = {
|
|
|
71
71
|
} as const satisfies Record<OnboardingGoal, readonly string[]>;
|
|
72
72
|
|
|
73
73
|
const MISSIONS = [
|
|
74
|
+
['colab', 'learn', 'reversible', ['prompt', 'simulate', 'review']],
|
|
74
75
|
['platform', 'focus', 'reversible', ['overview', 'apps', 'next_move']],
|
|
75
76
|
['calendar', 'focus', 'reversible', ['schedule', 'timebox', 'review']],
|
|
76
77
|
[
|
|
@@ -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.
|
|
10
|
+
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.46.0'); // x-release-please-version
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it('preserves the source timestamp in builtAt and derives a short hash', () => {
|
package/src/platform-release.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
|
|
2
2
|
|
|
3
|
-
export const TUTURUUU_PLATFORM_VERSION = '0.
|
|
3
|
+
export const TUTURUUU_PLATFORM_VERSION = '0.46.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
|
@@ -11,6 +11,7 @@ export const TUTURUUU_PORTLESS_APP_ORIGINS = {
|
|
|
11
11
|
calendar: `https://calendar.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
12
12
|
chat: `https://chat.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
13
13
|
cms: `https://cms.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
14
|
+
colab: `https://colab.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
14
15
|
contacts: `https://contacts.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
15
16
|
docs: `https://docs.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
16
17
|
drive: `https://drive.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
package/src/workspace-helper.ts
CHANGED
|
@@ -28,6 +28,20 @@ export class WorkspaceAuthError extends Error {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export class WorkspaceNotFoundError extends Error {
|
|
32
|
+
constructor(message = 'Workspace not found') {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'WorkspaceNotFoundError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class WorkspaceResolutionError extends Error {
|
|
39
|
+
constructor(message = 'Workspace resolution failed') {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'WorkspaceResolutionError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
export class WorkspaceAccessError extends Error {
|
|
32
46
|
constructor(message = 'Workspace access denied') {
|
|
33
47
|
super(message);
|
|
@@ -1096,7 +1110,7 @@ export async function normalizeWorkspaceId(
|
|
|
1096
1110
|
const principal = await resolveAuthenticatedPrincipal(sb);
|
|
1097
1111
|
|
|
1098
1112
|
if (!principal) {
|
|
1099
|
-
throw new
|
|
1113
|
+
throw new WorkspaceAuthError();
|
|
1100
1114
|
}
|
|
1101
1115
|
|
|
1102
1116
|
const userId = principal.id;
|
|
@@ -1109,8 +1123,11 @@ export async function normalizeWorkspaceId(
|
|
|
1109
1123
|
.eq('workspace_members.type', 'MEMBER')
|
|
1110
1124
|
.maybeSingle();
|
|
1111
1125
|
|
|
1112
|
-
if (error
|
|
1113
|
-
throw new
|
|
1126
|
+
if (error) {
|
|
1127
|
+
throw new WorkspaceResolutionError('Personal workspace lookup failed');
|
|
1128
|
+
}
|
|
1129
|
+
if (!workspace) {
|
|
1130
|
+
throw new WorkspaceNotFoundError('Personal workspace not found');
|
|
1114
1131
|
}
|
|
1115
1132
|
|
|
1116
1133
|
return workspace.id;
|