@tuturuuu/utils 0.28.0 → 0.29.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 +38 -0
- package/biome.json +1 -1
- package/package.json +17 -16
- package/src/__tests__/api-proxy-guard.test.ts +20 -27
- package/src/__tests__/app-url.test.ts +1 -0
- package/src/__tests__/workspace-principal-resolution.test.ts +34 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +107 -0
- package/src/abuse-protection/reputation.ts +42 -3
- package/src/api-proxy-guard.ts +0 -15
- package/src/commercial-comparison-0.ts +294 -0
- package/src/commercial-comparison-1.ts +283 -0
- package/src/commercial-comparison-2.ts +262 -0
- package/src/commercial-comparison-types.ts +20 -0
- package/src/commercial-comparison.test.ts +32 -0
- package/src/commercial-comparison.ts +96 -0
- package/src/coordination-protocol.ts +61 -0
- package/src/coordination.test.ts +84 -0
- package/src/coordination.ts +57 -0
- package/src/feature-tier-policy.ts +26 -0
- package/src/internal-domains.ts +3 -0
- package/src/launchable-apps.ts +13 -0
- package/src/meet-creation-policy.test.ts +11 -0
- package/src/meet-creation-policy.ts +9 -3
- package/src/meet-duration.ts +71 -0
- package/src/meet-hosting.test.ts +58 -0
- package/src/meet-hosting.ts +19 -0
- package/src/meeting-invitations.test.ts +102 -0
- package/src/meeting-invitations.ts +81 -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/task-helper/task-sort.test.ts +22 -3
- package/src/task-helper/task-sort.ts +33 -3
- package/src/workspace-helper.ts +3 -3
- package/src/workspace-limits.ts +1 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import type { ComparisonFeature } from './commercial-comparison-types';
|
|
2
|
+
export const features2: ComparisonFeature[] = [
|
|
3
|
+
{
|
|
4
|
+
id: 'lettin_0',
|
|
5
|
+
app: 'lettin',
|
|
6
|
+
category: 'learning',
|
|
7
|
+
values: ['preview', 'preview', 'preview', 'preview'],
|
|
8
|
+
detail: 'standard',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: 'colab_0',
|
|
12
|
+
app: 'colab',
|
|
13
|
+
category: 'learning',
|
|
14
|
+
values: ['included', 'included', 'included', 'included'],
|
|
15
|
+
detail: 'ai',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'colab_1',
|
|
19
|
+
app: 'colab',
|
|
20
|
+
category: 'learning',
|
|
21
|
+
values: ['included', 'included', 'included', 'included'],
|
|
22
|
+
detail: 'ai',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'colab_2',
|
|
26
|
+
app: 'colab',
|
|
27
|
+
category: 'learning',
|
|
28
|
+
values: ['included', 'included', 'included', 'included'],
|
|
29
|
+
detail: 'ai',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'colab_3',
|
|
33
|
+
app: 'colab',
|
|
34
|
+
category: 'learning',
|
|
35
|
+
values: ['included', 'included', 'included', 'included'],
|
|
36
|
+
detail: 'ai',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'learn_0',
|
|
40
|
+
app: 'learn',
|
|
41
|
+
category: 'learning',
|
|
42
|
+
values: ['preview', 'preview', 'preview', 'preview'],
|
|
43
|
+
detail: 'standard',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'teach_0',
|
|
47
|
+
app: 'teach',
|
|
48
|
+
category: 'learning',
|
|
49
|
+
values: ['preview', 'preview', 'preview', 'preview'],
|
|
50
|
+
detail: 'standard',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'rewise_0',
|
|
54
|
+
app: 'rewise',
|
|
55
|
+
category: 'learning',
|
|
56
|
+
values: ['preview', 'preview', 'preview', 'preview'],
|
|
57
|
+
detail: 'standard',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'nova_0',
|
|
61
|
+
app: 'nova',
|
|
62
|
+
category: 'learning',
|
|
63
|
+
values: ['preview', 'preview', 'preview', 'preview'],
|
|
64
|
+
detail: 'standard',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'git_0',
|
|
68
|
+
app: 'git',
|
|
69
|
+
category: 'developer',
|
|
70
|
+
values: ['included', 'included', 'included', 'included'],
|
|
71
|
+
detail: 'standard',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'git_1',
|
|
75
|
+
app: 'git',
|
|
76
|
+
category: 'developer',
|
|
77
|
+
values: ['included', 'included', 'included', 'included'],
|
|
78
|
+
detail: 'standard',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'git_2',
|
|
82
|
+
app: 'git',
|
|
83
|
+
category: 'developer',
|
|
84
|
+
values: ['included', 'included', 'included', 'included'],
|
|
85
|
+
detail: 'standard',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'external_0',
|
|
89
|
+
app: 'external',
|
|
90
|
+
category: 'developer',
|
|
91
|
+
values: ['included', 'included', 'included', 'included'],
|
|
92
|
+
detail: 'standard',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'external_1',
|
|
96
|
+
app: 'external',
|
|
97
|
+
category: 'developer',
|
|
98
|
+
values: ['included', 'included', 'included', 'included'],
|
|
99
|
+
detail: 'standard',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'mobile_0',
|
|
103
|
+
app: 'mobile',
|
|
104
|
+
category: 'work',
|
|
105
|
+
values: ['included', 'included', 'included', 'included'],
|
|
106
|
+
detail: 'standard',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'mobile_1',
|
|
110
|
+
app: 'mobile',
|
|
111
|
+
category: 'work',
|
|
112
|
+
values: ['included', 'included', 'included', 'included'],
|
|
113
|
+
detail: 'standard',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: 'pay_0',
|
|
117
|
+
app: 'pay',
|
|
118
|
+
category: 'operations',
|
|
119
|
+
values: ['included', 'included', 'included', 'included'],
|
|
120
|
+
detail: 'standard',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'pay_1',
|
|
124
|
+
app: 'pay',
|
|
125
|
+
category: 'operations',
|
|
126
|
+
values: ['included', 'included', 'included', 'included'],
|
|
127
|
+
detail: 'standard',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'pay_2',
|
|
131
|
+
app: 'pay',
|
|
132
|
+
category: 'operations',
|
|
133
|
+
values: ['included', 'included', 'included', 'included'],
|
|
134
|
+
detail: 'standard',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'apps_0',
|
|
138
|
+
app: 'apps',
|
|
139
|
+
category: 'work',
|
|
140
|
+
values: ['included', 'included', 'included', 'included'],
|
|
141
|
+
detail: 'standard',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'tools_0',
|
|
145
|
+
app: 'tools',
|
|
146
|
+
category: 'work',
|
|
147
|
+
values: ['included', 'included', 'included', 'included'],
|
|
148
|
+
detail: 'standard',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'shortener_0',
|
|
152
|
+
app: 'shortener',
|
|
153
|
+
category: 'work',
|
|
154
|
+
values: ['included', 'included', 'included', 'included'],
|
|
155
|
+
detail: 'standard',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'storefront_0',
|
|
159
|
+
app: 'storefront',
|
|
160
|
+
category: 'operations',
|
|
161
|
+
values: ['preview', 'preview', 'preview', 'preview'],
|
|
162
|
+
detail: 'standard',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'playground_0',
|
|
166
|
+
app: 'playground',
|
|
167
|
+
category: 'developer',
|
|
168
|
+
values: ['preview', 'preview', 'preview', 'preview'],
|
|
169
|
+
detail: 'standard',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'discord_0',
|
|
173
|
+
app: 'discord',
|
|
174
|
+
category: 'developer',
|
|
175
|
+
values: ['preview', 'preview', 'preview', 'preview'],
|
|
176
|
+
detail: 'standard',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'docs_0',
|
|
180
|
+
app: 'docs',
|
|
181
|
+
category: 'developer',
|
|
182
|
+
values: ['included', 'included', 'included', 'included'],
|
|
183
|
+
detail: 'standard',
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'backend_0',
|
|
187
|
+
app: 'backend',
|
|
188
|
+
category: 'internal',
|
|
189
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
190
|
+
detail: 'standard',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'database_0',
|
|
194
|
+
app: 'database',
|
|
195
|
+
category: 'internal',
|
|
196
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
197
|
+
detail: 'standard',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'redis_0',
|
|
201
|
+
app: 'redis',
|
|
202
|
+
category: 'internal',
|
|
203
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
204
|
+
detail: 'standard',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'supermemory_0',
|
|
208
|
+
app: 'supermemory',
|
|
209
|
+
category: 'internal',
|
|
210
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
211
|
+
detail: 'standard',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'storage-unzip-proxy_0',
|
|
215
|
+
app: 'storage-unzip-proxy',
|
|
216
|
+
category: 'internal',
|
|
217
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
218
|
+
detail: 'standard',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'chat-realtime_0',
|
|
222
|
+
app: 'chat-realtime',
|
|
223
|
+
category: 'internal',
|
|
224
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
225
|
+
detail: 'standard',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
id: 'coordination_0',
|
|
229
|
+
app: 'coordination',
|
|
230
|
+
category: 'internal',
|
|
231
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
232
|
+
detail: 'standard',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'meet-realtime_0',
|
|
236
|
+
app: 'meet-realtime',
|
|
237
|
+
category: 'internal',
|
|
238
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
239
|
+
detail: 'standard',
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: 'hive-realtime_0',
|
|
243
|
+
app: 'hive-realtime',
|
|
244
|
+
category: 'internal',
|
|
245
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
246
|
+
detail: 'standard',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'tanstack-web_0',
|
|
250
|
+
app: 'tanstack-web',
|
|
251
|
+
category: 'internal',
|
|
252
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
253
|
+
detail: 'standard',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: 'infrastructure_0',
|
|
257
|
+
app: 'infrastructure',
|
|
258
|
+
category: 'internal',
|
|
259
|
+
values: ['internal', 'internal', 'internal', 'internal'],
|
|
260
|
+
detail: 'standard',
|
|
261
|
+
},
|
|
262
|
+
];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FEATURE_TIERS } from './feature-tier-policy';
|
|
2
|
+
/** Source-backed feature inventory. Preview is availability, never an upgrade promise.
|
|
3
|
+
* Navigation and feature-tiers.ts own capabilities; capacity rows identify pending rollout.
|
|
4
|
+
* Internal services share the host workspace budget and are not separately sold. */
|
|
5
|
+
export type ComparisonTier = 'free' | 'plus' | 'pro' | 'enterprise';
|
|
6
|
+
export const COMPARISON_TIERS: ComparisonTier[] = [
|
|
7
|
+
'free',
|
|
8
|
+
'plus',
|
|
9
|
+
'pro',
|
|
10
|
+
'enterprise',
|
|
11
|
+
];
|
|
12
|
+
export interface ComparisonFeature {
|
|
13
|
+
id: string;
|
|
14
|
+
app: string;
|
|
15
|
+
category: string;
|
|
16
|
+
values: [string, string, string, string];
|
|
17
|
+
detail: string;
|
|
18
|
+
pending?: boolean;
|
|
19
|
+
gate?: keyof typeof FEATURE_TIERS;
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { COMPARISON_FEATURES } from './commercial-comparison';
|
|
5
|
+
|
|
6
|
+
describe('commercial inventory coverage', () => {
|
|
7
|
+
it('represents every app directory and avoids duplicate feature identifiers', () => {
|
|
8
|
+
const apps = readdirSync(resolve(__dirname, '../../../apps'), {
|
|
9
|
+
withFileTypes: true,
|
|
10
|
+
})
|
|
11
|
+
.filter((item) => item.isDirectory() && !item.name.startsWith('.'))
|
|
12
|
+
.map((item) => (item.name === 'web' ? 'platform' : item.name));
|
|
13
|
+
const represented = new Set(COMPARISON_FEATURES.map((row) => row.app));
|
|
14
|
+
expect(apps.filter((app) => !represented.has(app))).toEqual([]);
|
|
15
|
+
expect(new Set(COMPARISON_FEATURES.map((row) => row.id)).size).toBe(
|
|
16
|
+
COMPARISON_FEATURES.length
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
it('does not sell privileged internal services or promise public Meet access', () => {
|
|
20
|
+
for (const row of COMPARISON_FEATURES.filter(
|
|
21
|
+
(row) => row.category === 'internal'
|
|
22
|
+
))
|
|
23
|
+
expect(row.values).toEqual([
|
|
24
|
+
'internal',
|
|
25
|
+
'internal',
|
|
26
|
+
'internal',
|
|
27
|
+
'internal',
|
|
28
|
+
]);
|
|
29
|
+
for (const row of COMPARISON_FEATURES.filter((row) => row.app === 'meet'))
|
|
30
|
+
expect(row.values).toEqual(['preview', 'preview', 'preview', 'preview']);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ComparisonFeature } from './commercial-comparison-types';
|
|
2
|
+
import { FEATURE_TIERS } from './feature-tier-policy';
|
|
3
|
+
|
|
4
|
+
export * from './commercial-comparison-types';
|
|
5
|
+
|
|
6
|
+
import { features0 } from './commercial-comparison-0';
|
|
7
|
+
import { features1 } from './commercial-comparison-1';
|
|
8
|
+
import { features2 } from './commercial-comparison-2';
|
|
9
|
+
|
|
10
|
+
const gateOrder = ['FREE', 'PLUS', 'PRO', 'ENTERPRISE'];
|
|
11
|
+
const advanced: ComparisonFeature[] = [
|
|
12
|
+
{
|
|
13
|
+
id: 'prioritySupport',
|
|
14
|
+
app: 'platform',
|
|
15
|
+
category: 'operations',
|
|
16
|
+
detail: 'support',
|
|
17
|
+
values: ['excluded', 'excluded', 'included', 'contract'],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'customIntegrations',
|
|
21
|
+
app: 'platform',
|
|
22
|
+
category: 'operations',
|
|
23
|
+
detail: 'support',
|
|
24
|
+
values: ['excluded', 'excluded', 'preview', 'contract'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'dedicatedSupport',
|
|
28
|
+
app: 'platform',
|
|
29
|
+
category: 'operations',
|
|
30
|
+
detail: 'support',
|
|
31
|
+
values: ['excluded', 'excluded', 'excluded', 'contract'],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'sla',
|
|
35
|
+
app: 'platform',
|
|
36
|
+
category: 'operations',
|
|
37
|
+
detail: 'support',
|
|
38
|
+
values: ['excluded', 'excluded', 'excluded', 'contract'],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'selfHosting',
|
|
42
|
+
app: 'platform',
|
|
43
|
+
category: 'operations',
|
|
44
|
+
detail: 'support',
|
|
45
|
+
values: ['excluded', 'excluded', 'excluded', 'contract'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'aiLab',
|
|
49
|
+
app: 'platform',
|
|
50
|
+
category: 'work',
|
|
51
|
+
detail: 'standard',
|
|
52
|
+
values: ['included', 'included', 'included', 'included'],
|
|
53
|
+
gate: 'ai_lab',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'voiceAssistant',
|
|
57
|
+
app: 'platform',
|
|
58
|
+
category: 'work',
|
|
59
|
+
detail: 'standard',
|
|
60
|
+
values: ['included', 'included', 'included', 'included'],
|
|
61
|
+
gate: 'voice_assistant',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'mira',
|
|
65
|
+
app: 'platform',
|
|
66
|
+
category: 'work',
|
|
67
|
+
detail: 'standard',
|
|
68
|
+
values: ['included', 'included', 'included', 'included'],
|
|
69
|
+
gate: 'mira',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'workforce',
|
|
73
|
+
app: 'platform',
|
|
74
|
+
category: 'work',
|
|
75
|
+
detail: 'standard',
|
|
76
|
+
values: ['included', 'included', 'included', 'included'],
|
|
77
|
+
gate: 'workforce',
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
export const COMPARISON_FEATURES = [
|
|
81
|
+
...features0,
|
|
82
|
+
...features1,
|
|
83
|
+
...features2,
|
|
84
|
+
...advanced,
|
|
85
|
+
].map((row) => {
|
|
86
|
+
if (!row.gate) return row;
|
|
87
|
+
const required = FEATURE_TIERS[row.gate];
|
|
88
|
+
return {
|
|
89
|
+
...row,
|
|
90
|
+
values: gateOrder.map((tier) =>
|
|
91
|
+
gateOrder.indexOf(tier) >= gateOrder.indexOf(required)
|
|
92
|
+
? 'included'
|
|
93
|
+
: 'excluded'
|
|
94
|
+
) as ComparisonFeature['values'],
|
|
95
|
+
};
|
|
96
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
4
|
+
const base = {
|
|
5
|
+
key: digest,
|
|
6
|
+
owner: z.string().uuid(),
|
|
7
|
+
};
|
|
8
|
+
export const coordinationRequestSchema = z.union([
|
|
9
|
+
z
|
|
10
|
+
.object({
|
|
11
|
+
...base,
|
|
12
|
+
namespace: z.literal('authenticator'),
|
|
13
|
+
action: z.literal('acquire'),
|
|
14
|
+
})
|
|
15
|
+
.strict(),
|
|
16
|
+
z
|
|
17
|
+
.object({
|
|
18
|
+
...base,
|
|
19
|
+
namespace: z.literal('meeting'),
|
|
20
|
+
action: z.literal('acquire'),
|
|
21
|
+
fingerprint: digest,
|
|
22
|
+
})
|
|
23
|
+
.strict(),
|
|
24
|
+
z
|
|
25
|
+
.object({
|
|
26
|
+
...base,
|
|
27
|
+
namespace: z.enum(['authenticator', 'meeting']),
|
|
28
|
+
action: z.enum(['check', 'release']),
|
|
29
|
+
})
|
|
30
|
+
.strict(),
|
|
31
|
+
z
|
|
32
|
+
.object({
|
|
33
|
+
...base,
|
|
34
|
+
namespace: z.literal('meeting'),
|
|
35
|
+
action: z.literal('complete'),
|
|
36
|
+
})
|
|
37
|
+
.strict(),
|
|
38
|
+
]);
|
|
39
|
+
export type CoordinationRequest = z.infer<typeof coordinationRequestSchema>;
|
|
40
|
+
export const coordinationResultSchema = z.union([
|
|
41
|
+
z
|
|
42
|
+
.object({
|
|
43
|
+
outcome: z.literal('acquired'),
|
|
44
|
+
fresh: z.boolean(),
|
|
45
|
+
completed: z.boolean(),
|
|
46
|
+
})
|
|
47
|
+
.strict(),
|
|
48
|
+
z
|
|
49
|
+
.object({
|
|
50
|
+
outcome: z.enum([
|
|
51
|
+
'busy',
|
|
52
|
+
'conflict',
|
|
53
|
+
'owned',
|
|
54
|
+
'lost',
|
|
55
|
+
'released',
|
|
56
|
+
'completed',
|
|
57
|
+
]),
|
|
58
|
+
})
|
|
59
|
+
.strict(),
|
|
60
|
+
]);
|
|
61
|
+
export type CoordinationResult = z.infer<typeof coordinationResultSchema>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('server-only', () => ({}));
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
CoordinationUnavailableError,
|
|
7
|
+
coordinate,
|
|
8
|
+
coordinationKey,
|
|
9
|
+
} from './coordination';
|
|
10
|
+
|
|
11
|
+
const input = {
|
|
12
|
+
namespace: 'authenticator' as const,
|
|
13
|
+
key: 'a'.repeat(64),
|
|
14
|
+
owner: '0199619a-9154-7000-8000-000000000001',
|
|
15
|
+
action: 'acquire' as const,
|
|
16
|
+
};
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.unstubAllGlobals();
|
|
19
|
+
vi.unstubAllEnvs();
|
|
20
|
+
});
|
|
21
|
+
function setup() {
|
|
22
|
+
vi.stubEnv('CLOUDFLARE_COORDINATION_URL', 'https://coordination.example.com');
|
|
23
|
+
vi.stubEnv(
|
|
24
|
+
'CLOUDFLARE_COORDINATION_TOKEN',
|
|
25
|
+
'test-only-placeholder'.repeat(3)
|
|
26
|
+
);
|
|
27
|
+
const fetchMock = vi
|
|
28
|
+
.fn()
|
|
29
|
+
.mockResolvedValue(
|
|
30
|
+
Response.json({ outcome: 'acquired', fresh: true, completed: false })
|
|
31
|
+
);
|
|
32
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
33
|
+
return fetchMock;
|
|
34
|
+
}
|
|
35
|
+
describe('server coordination client', () => {
|
|
36
|
+
it('hashes identities and sends bounded uncached authenticated requests', async () => {
|
|
37
|
+
const fetchMock = setup();
|
|
38
|
+
expect(coordinationKey('user')).toMatch(/^[a-f0-9]{64}$/);
|
|
39
|
+
await expect(coordinate(input)).resolves.toMatchObject({
|
|
40
|
+
outcome: 'acquired',
|
|
41
|
+
});
|
|
42
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
43
|
+
new URL('https://coordination.example.com/v1/coordinate'),
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
cache: 'no-store',
|
|
46
|
+
redirect: 'error',
|
|
47
|
+
method: 'POST',
|
|
48
|
+
signal: expect.any(AbortSignal),
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
it('fails closed without configuration or with an insecure URL', async () => {
|
|
53
|
+
const fetchMock = setup();
|
|
54
|
+
for (const url of [
|
|
55
|
+
'',
|
|
56
|
+
'http://example.com',
|
|
57
|
+
'https://user:password@example.com',
|
|
58
|
+
]) {
|
|
59
|
+
vi.stubEnv('CLOUDFLARE_COORDINATION_URL', url);
|
|
60
|
+
await expect(coordinate(input)).rejects.toBeInstanceOf(
|
|
61
|
+
CoordinationUnavailableError
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
it('sanitizes provider failures and does not retry ambiguous mutations', async () => {
|
|
67
|
+
const fetchMock = setup();
|
|
68
|
+
fetchMock.mockRejectedValueOnce(new Error('sensitive provider detail'));
|
|
69
|
+
await expect(coordinate(input)).rejects.toThrow(
|
|
70
|
+
'Shared coordination is temporarily unavailable'
|
|
71
|
+
);
|
|
72
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
73
|
+
fetchMock.mockResolvedValueOnce(Response.json({ unexpected: 'value' }));
|
|
74
|
+
await expect(coordinate(input)).rejects.toBeInstanceOf(
|
|
75
|
+
CoordinationUnavailableError
|
|
76
|
+
);
|
|
77
|
+
fetchMock.mockResolvedValueOnce(
|
|
78
|
+
new Response('private error', { status: 503 })
|
|
79
|
+
);
|
|
80
|
+
await expect(coordinate(input)).rejects.toBeInstanceOf(
|
|
81
|
+
CoordinationUnavailableError
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
type CoordinationRequest,
|
|
6
|
+
coordinationResultSchema,
|
|
7
|
+
} from './coordination-protocol';
|
|
8
|
+
import { Effect } from './effect';
|
|
9
|
+
|
|
10
|
+
export class CoordinationUnavailableError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super('Shared coordination is temporarily unavailable');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function coordinationKey(value: string) {
|
|
16
|
+
return createHash('sha256').update(value).digest('hex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Server-only, bounded requests. Do not retry mutations after an ambiguous
|
|
20
|
+
* network failure: their expiring lease may already have been committed. */
|
|
21
|
+
export async function coordinate(input: CoordinationRequest) {
|
|
22
|
+
const result = await Effect.runPromise(
|
|
23
|
+
Effect.either(
|
|
24
|
+
Effect.tryPromise({
|
|
25
|
+
try: async () => {
|
|
26
|
+
const origin = new URL(process.env.CLOUDFLARE_COORDINATION_URL ?? '');
|
|
27
|
+
const token = process.env.CLOUDFLARE_COORDINATION_TOKEN;
|
|
28
|
+
if (
|
|
29
|
+
origin.protocol !== 'https:' ||
|
|
30
|
+
origin.username ||
|
|
31
|
+
origin.password ||
|
|
32
|
+
!token ||
|
|
33
|
+
token.length < 32
|
|
34
|
+
)
|
|
35
|
+
throw new CoordinationUnavailableError();
|
|
36
|
+
const response = await fetch(new URL('/v1/coordinate', origin), {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
Authorization: `Bearer ${token}`,
|
|
40
|
+
'Content-Type': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(input),
|
|
43
|
+
cache: 'no-store',
|
|
44
|
+
redirect: 'error',
|
|
45
|
+
signal: AbortSignal.timeout(5000),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) throw new CoordinationUnavailableError();
|
|
48
|
+
return coordinationResultSchema.parse(await response.json());
|
|
49
|
+
},
|
|
50
|
+
// Never expose URLs, request bodies, or authorization headers in errors.
|
|
51
|
+
catch: () => new CoordinationUnavailableError(),
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
if (result._tag === 'Left') throw result.left;
|
|
56
|
+
return result.right;
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { WorkspaceProductTier } from '@tuturuuu/types/db';
|
|
2
|
+
export const FEATURE_TIERS = {
|
|
3
|
+
// FREE tier features
|
|
4
|
+
dashboard: 'FREE',
|
|
5
|
+
tasks: 'FREE',
|
|
6
|
+
calendar: 'FREE',
|
|
7
|
+
notifications: 'FREE',
|
|
8
|
+
qr_generator: 'FREE',
|
|
9
|
+
finance: 'FREE',
|
|
10
|
+
users: 'FREE',
|
|
11
|
+
inventory: 'FREE',
|
|
12
|
+
whiteboards: 'FREE',
|
|
13
|
+
drive: 'FREE',
|
|
14
|
+
realtime_cursors: 'FREE',
|
|
15
|
+
documents: 'FREE',
|
|
16
|
+
time_tracker: 'FREE',
|
|
17
|
+
voice_assistant: 'FREE',
|
|
18
|
+
|
|
19
|
+
// PLUS tier features
|
|
20
|
+
chat: 'PLUS',
|
|
21
|
+
|
|
22
|
+
// PRO tier features
|
|
23
|
+
workforce: 'PRO',
|
|
24
|
+
ai_lab: 'PRO',
|
|
25
|
+
mira: 'PRO',
|
|
26
|
+
} as const satisfies Record<string, WorkspaceProductTier>;
|
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: 'lettin', url: 'https://lettin.tuturuuu.com' },
|
|
7
8
|
{ name: 'colab', url: 'https://colab.tuturuuu.com' },
|
|
8
9
|
{
|
|
9
10
|
name: 'ai',
|
|
@@ -132,6 +133,7 @@ export const PRODUCTION_INTERNAL_APP_DOMAINS = [
|
|
|
132
133
|
] as const;
|
|
133
134
|
|
|
134
135
|
export const PORTLESS_INTERNAL_APP_DOMAINS = [
|
|
136
|
+
{ name: 'lettin', url: getTuturuuuPortlessAppOrigin('lettin') },
|
|
135
137
|
{ name: 'colab', url: getTuturuuuPortlessAppOrigin('colab') },
|
|
136
138
|
{
|
|
137
139
|
name: 'ai',
|
|
@@ -252,6 +254,7 @@ export const PORTLESS_INTERNAL_APP_DOMAINS = [
|
|
|
252
254
|
] as const;
|
|
253
255
|
|
|
254
256
|
export const LOCALHOST_INTERNAL_APP_DOMAINS = [
|
|
257
|
+
{ name: 'lettin', url: 'http://localhost:7833' },
|
|
255
258
|
{ name: 'colab', url: 'http://localhost:8795' },
|
|
256
259
|
{
|
|
257
260
|
name: 'ai',
|
package/src/launchable-apps.ts
CHANGED
|
@@ -399,6 +399,19 @@ export const LAUNCHABLE_APPS = [
|
|
|
399
399
|
slug: 'colab',
|
|
400
400
|
title: 'Colab',
|
|
401
401
|
},
|
|
402
|
+
{
|
|
403
|
+
aliases: ['Worldbuilding', 'Wiki', 'Characters', 'Stories', 'Bulletin'],
|
|
404
|
+
appRoot: 'apps/lettin',
|
|
405
|
+
category: 'productivity',
|
|
406
|
+
defaultPath: '/dashboard',
|
|
407
|
+
localhostOrigin: 'http://localhost:7833',
|
|
408
|
+
packageName: '@tuturuuu/lettin',
|
|
409
|
+
portlessApp: 'lettin',
|
|
410
|
+
productionUrl: 'https://lettin.tuturuuu.com',
|
|
411
|
+
slug: 'lettin',
|
|
412
|
+
title: '(Tu)lettin',
|
|
413
|
+
workspacePathResolver: workspaceRootPath,
|
|
414
|
+
},
|
|
402
415
|
] as const satisfies readonly LaunchableApp[];
|
|
403
416
|
|
|
404
417
|
export type LaunchableAppSlug = (typeof LAUNCHABLE_APPS)[number]['slug'];
|