@sovereignfs/sdk 1.25.0 → 2.0.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.
Files changed (55) hide show
  1. package/LICENSE +373 -661
  2. package/README.md +103 -0
  3. package/dist/browser.d.ts +2 -0
  4. package/dist/browser.js +4 -0
  5. package/dist/chunk-NYL4LLXF.js +65 -0
  6. package/dist/index.d.ts +57 -529
  7. package/dist/index.js +1283 -572
  8. package/dist/next.d.ts +37 -0
  9. package/dist/next.js +68 -0
  10. package/dist/session-DJl2XYMc.d.ts +44 -0
  11. package/dist/sovereign-CazGKEVc.d.ts +517 -0
  12. package/dist/testing.d.ts +181 -0
  13. package/dist/testing.js +560 -0
  14. package/package.json +53 -27
  15. package/dist/chunk-MLKGABMK.js +0 -9
  16. package/dist/chunk-NQLGPAL6.js +0 -62
  17. package/dist/e2ee-crypto.d.ts +0 -84
  18. package/dist/e2ee-crypto.js +0 -162
  19. package/dist/e2ee-device.d.ts +0 -21
  20. package/dist/e2ee-device.js +0 -13
  21. package/dist/e2ee-object.d.ts +0 -50
  22. package/dist/e2ee-object.js +0 -75
  23. package/dist/e2ee-state-U-GEJ7oL.d.ts +0 -411
  24. package/dist/e2ee-state.d.ts +0 -1
  25. package/dist/e2ee-state.js +0 -36
  26. package/src/__tests__/e2ee-crypto.test.ts +0 -156
  27. package/src/__tests__/e2ee-object.test.ts +0 -90
  28. package/src/__tests__/e2ee-state.test.ts +0 -83
  29. package/src/__tests__/sdk.test.ts +0 -427
  30. package/src/__tests__/sessions.test.ts +0 -63
  31. package/src/activity.ts +0 -19
  32. package/src/auth.ts +0 -134
  33. package/src/connections.ts +0 -106
  34. package/src/data.ts +0 -88
  35. package/src/db.ts +0 -34
  36. package/src/directory.ts +0 -31
  37. package/src/e2ee-crypto.ts +0 -260
  38. package/src/e2ee-device.ts +0 -75
  39. package/src/e2ee-object.ts +0 -118
  40. package/src/e2ee-state.ts +0 -72
  41. package/src/e2ee.ts +0 -65
  42. package/src/env.ts +0 -37
  43. package/src/errors.ts +0 -46
  44. package/src/host.ts +0 -251
  45. package/src/index.ts +0 -160
  46. package/src/mailer.ts +0 -7
  47. package/src/notifications.ts +0 -36
  48. package/src/platform.ts +0 -10
  49. package/src/plugins.ts +0 -146
  50. package/src/portability.ts +0 -176
  51. package/src/secrets.ts +0 -68
  52. package/src/sessions.ts +0 -36
  53. package/src/storage.ts +0 -49
  54. package/src/types.ts +0 -418
  55. package/src/unimplemented.ts +0 -48
@@ -1,83 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import type { E2eeDeviceEnrollment, E2eeProfile } from '../types';
3
- import { classifyE2eeLocalState } from '../e2ee-state';
4
-
5
- const profile: E2eeProfile = {
6
- id: 'profile-1',
7
- userId: 'user-1',
8
- status: 'active',
9
- cmkAlgorithm: 'AES-GCM-256',
10
- createdAt: 0,
11
- updatedAt: 0,
12
- };
13
-
14
- function enrollment(overrides: Partial<E2eeDeviceEnrollment> = {}): E2eeDeviceEnrollment {
15
- return {
16
- id: 'enrollment-1',
17
- userId: 'user-1',
18
- deviceId: 'device-1',
19
- deviceLabel: 'Chrome on macOS',
20
- wrappedCmk: 'opaque',
21
- algorithmVersion: 'v1',
22
- createdAt: 0,
23
- lastUsedAt: null,
24
- revokedAt: null,
25
- ...overrides,
26
- };
27
- }
28
-
29
- describe('classifyE2eeLocalState', () => {
30
- it('returns not-set-up when there is no profile', () => {
31
- const result = classifyE2eeLocalState(null, [], 'device-1', null);
32
- expect(result).toEqual({
33
- state: 'not-set-up',
34
- deviceId: null,
35
- deviceKey: null,
36
- activeEnrollment: null,
37
- });
38
- });
39
-
40
- it('returns locked when a profile exists but this device has no local key', () => {
41
- const result = classifyE2eeLocalState(profile, [enrollment()], 'device-1', null);
42
- expect(result.state).toBe('locked');
43
- expect(result.deviceKey).toBeNull();
44
- });
45
-
46
- it('returns locked when this device has a local key but no active enrollment record', () => {
47
- const fakeKey = {} as CryptoKey;
48
- const result = classifyE2eeLocalState(profile, [], 'device-1', fakeKey);
49
- expect(result.state).toBe('locked');
50
- });
51
-
52
- it('returns locked when this device has a local key but its enrollment was revoked', () => {
53
- const fakeKey = {} as CryptoKey;
54
- const result = classifyE2eeLocalState(
55
- profile,
56
- [enrollment({ revokedAt: 12345 })],
57
- 'device-1',
58
- fakeKey,
59
- );
60
- expect(result.state).toBe('locked');
61
- });
62
-
63
- it('returns unlocked when this device has both a local key and an active enrollment', () => {
64
- const fakeKey = {} as CryptoKey;
65
- const activeEnrollment = enrollment();
66
- const result = classifyE2eeLocalState(profile, [activeEnrollment], 'device-1', fakeKey);
67
-
68
- expect(result).toEqual({
69
- state: 'unlocked',
70
- deviceId: 'device-1',
71
- deviceKey: fakeKey,
72
- activeEnrollment,
73
- });
74
- });
75
-
76
- it('only matches the enrollment for this specific device id', () => {
77
- const fakeKey = {} as CryptoKey;
78
- const otherDeviceEnrollment = enrollment({ deviceId: 'device-2' });
79
- const result = classifyE2eeLocalState(profile, [otherDeviceEnrollment], 'device-1', fakeKey);
80
-
81
- expect(result.state).toBe('locked');
82
- });
83
- });
@@ -1,427 +0,0 @@
1
- import { beforeAll, describe, expect, it } from 'vitest';
2
- import { provideHost } from '../host';
3
- import { ConsentRequiredError, NotAuthenticatedError, NotImplementedError, sdk } from '../index';
4
-
5
- // A minimal mock host — lets us test SDK delegation without a real runtime.
6
- const mockDbClient = { select: () => ({}), insert: () => ({}) };
7
- const mockConfig = {
8
- tenantName: 'Test Workspace',
9
- inviteOnly: false,
10
- version: '0.6.0',
11
- instanceName: 'Test Workspace',
12
- instanceId: 'test-instance-uuid',
13
- };
14
- const mockDataResolvers = new Map<string, (...args: unknown[]) => Promise<unknown[]>>();
15
- const mockExporters = new Map<string, unknown>();
16
- const mockImporters = new Map<string, unknown>();
17
-
18
- beforeAll(() => {
19
- provideHost({
20
- db: {
21
- async getClient() {
22
- return mockDbClient;
23
- },
24
- },
25
- mailer: {
26
- async send() {
27
- /* no-op */
28
- },
29
- },
30
- platform: {
31
- async getConfig() {
32
- return mockConfig;
33
- },
34
- },
35
- directory: {
36
- async searchUsers() {
37
- return [];
38
- },
39
- async resolveUsers() {
40
- return [];
41
- },
42
- },
43
- data: {
44
- provide(contract, resolver) {
45
- mockDataResolvers.set(contract, resolver as (...args: unknown[]) => Promise<unknown[]>);
46
- },
47
- async query(_ref, _consumerId, _userId, _tenantId, _params) {
48
- return [];
49
- },
50
- },
51
- activity: {
52
- async log(_entry, _actorId, _pluginId) {
53
- /* no-op */
54
- },
55
- },
56
- portability: {
57
- provideExport(pluginId, resolver) {
58
- mockExporters.set(pluginId, resolver);
59
- },
60
- provideImport(pluginId, handler) {
61
- mockImporters.set(pluginId, handler);
62
- },
63
- provideDelete(_pluginId, _handler) {
64
- /* no-op */
65
- },
66
- },
67
- plugins: {
68
- async get(id) {
69
- if (id !== 'com.example.notes') return null;
70
- return {
71
- id,
72
- name: 'Notes',
73
- routePrefix: '/notes',
74
- installed: true,
75
- enabled: true,
76
- availableToUser: true,
77
- providesContracts: [],
78
- };
79
- },
80
- async list() {
81
- return [];
82
- },
83
- async getConsentStatus() {
84
- return 'not_granted';
85
- },
86
- },
87
- notifications: {
88
- async send(_input, _pluginId) {
89
- /* no-op */
90
- },
91
- },
92
- secrets: {
93
- async create(input) {
94
- return {
95
- id: 'secret-1',
96
- scope: input.scope,
97
- label: input.label,
98
- metadata: input.metadata ?? null,
99
- createdAt: 1,
100
- updatedAt: 1,
101
- lastUsedAt: null,
102
- };
103
- },
104
- async get(_id) {
105
- return 'secret';
106
- },
107
- async list() {
108
- return [];
109
- },
110
- async update(_id, _value) {
111
- return {
112
- id: 'secret-1',
113
- scope: 'user',
114
- label: 'Updated',
115
- metadata: null,
116
- createdAt: 1,
117
- updatedAt: 2,
118
- lastUsedAt: null,
119
- };
120
- },
121
- async delete(_id) {
122
- /* no-op */
123
- },
124
- },
125
- connections: {
126
- async create(input) {
127
- return {
128
- id: 'conn-1',
129
- scope: input.scope,
130
- provider: input.provider,
131
- label: input.label,
132
- status: 'connected',
133
- secretRef: input.secretRef ?? null,
134
- metadata: input.metadata ?? null,
135
- lastCheckedAt: null,
136
- lastUsedAt: null,
137
- lastError: null,
138
- createdAt: 1,
139
- updatedAt: 1,
140
- disconnectedAt: null,
141
- };
142
- },
143
- async list() {
144
- return [];
145
- },
146
- async get() {
147
- return null;
148
- },
149
- async update(_id, input) {
150
- return {
151
- id: 'conn-1',
152
- scope: 'user',
153
- provider: 'email.google',
154
- label: input.label ?? 'Google Mail',
155
- status: input.status ?? 'connected',
156
- secretRef: input.secretRef ?? null,
157
- metadata: input.metadata ?? null,
158
- lastCheckedAt: input.lastCheckedAt ?? null,
159
- lastUsedAt: null,
160
- lastError: null,
161
- createdAt: 1,
162
- updatedAt: 2,
163
- disconnectedAt: null,
164
- };
165
- },
166
- async disconnect() {
167
- /* no-op */
168
- },
169
- async markUsed() {
170
- /* no-op */
171
- },
172
- async markError(_id, input) {
173
- return {
174
- id: 'conn-1',
175
- scope: 'user',
176
- provider: 'email.google',
177
- label: 'Google Mail',
178
- status: input.status ?? 'error',
179
- secretRef: null,
180
- metadata: null,
181
- lastCheckedAt: null,
182
- lastUsedAt: null,
183
- lastError: input.error,
184
- createdAt: 1,
185
- updatedAt: 2,
186
- disconnectedAt: null,
187
- };
188
- },
189
- async createOAuthState() {
190
- return 'state';
191
- },
192
- async verifyOAuthState() {
193
- return {
194
- pluginId: 'com.example.notes',
195
- provider: 'email.google',
196
- userId: 'u1',
197
- callbackPath: '/connections/google/callback',
198
- nonce: 'nonce',
199
- metadata: null,
200
- expiresAt: 2,
201
- };
202
- },
203
- async getProviderConfig(provider) {
204
- return {
205
- provider,
206
- label: 'Google Mail',
207
- configured: true,
208
- source: 'console',
209
- publicValues: { clientId: 'client-id' },
210
- secretValues: { clientSecret: 'client-secret' },
211
- callbackUrl: 'https://example.test/notes/connections/google/callback',
212
- scopes: ['user'],
213
- missingRequired: [],
214
- };
215
- },
216
- },
217
- storage: {
218
- async put(input) {
219
- return {
220
- id: 'obj-1',
221
- pluginId: 'com.example.notes',
222
- ownerUserId: input.ownerUserId ?? null,
223
- key: input.key,
224
- contentType: input.contentType,
225
- size: 3,
226
- checksum: 'checksum',
227
- metadata: input.metadata ?? null,
228
- createdAt: 1,
229
- updatedAt: 1,
230
- };
231
- },
232
- async get(_key) {
233
- return null;
234
- },
235
- async delete(_key) {
236
- /* no-op */
237
- },
238
- async list() {
239
- return [];
240
- },
241
- async getSignedUrl(_key) {
242
- return 'https://example.test/api/storage/signed-token';
243
- },
244
- },
245
- e2ee: {
246
- async getProfile() {
247
- return null;
248
- },
249
- async createProfile(input, context) {
250
- return {
251
- id: 'profile-1',
252
- userId: context.userId,
253
- status: 'active',
254
- cmkAlgorithm: input.cmkAlgorithm,
255
- createdAt: 1,
256
- updatedAt: 1,
257
- };
258
- },
259
- async getRecoveryWrapper() {
260
- return null;
261
- },
262
- async setRecoveryWrapper(input, context) {
263
- return { id: 'wrapper-1', userId: context.userId, ...input, createdAt: 1, updatedAt: 1 };
264
- },
265
- async enrollDevice(input, context) {
266
- return {
267
- id: 'device-1',
268
- userId: context.userId,
269
- ...input,
270
- createdAt: 1,
271
- lastUsedAt: null,
272
- revokedAt: null,
273
- };
274
- },
275
- async listDevices() {
276
- return [];
277
- },
278
- async revokeDevice(_id) {
279
- /* no-op */
280
- },
281
- },
282
- });
283
- });
284
-
285
- describe('sdk surface', () => {
286
- it('exposes the full v1 stable surface', () => {
287
- expect(typeof sdk.auth.getSession).toBe('function');
288
- expect(typeof sdk.auth.requireSession).toBe('function');
289
- expect(typeof sdk.auth.changePassword).toBe('function');
290
- expect(typeof sdk.auth.listSessions).toBe('function');
291
- expect(typeof sdk.auth.revokeSession).toBe('function');
292
- expect(typeof sdk.auth.signOut).toBe('function');
293
- expect(typeof sdk.db.getClient).toBe('function');
294
- expect(typeof sdk.mailer.send).toBe('function');
295
- expect(typeof sdk.platform.getConfig).toBe('function');
296
- });
297
-
298
- it('exposes the directory surface (RFC 0041)', () => {
299
- expect(typeof sdk.directory.searchUsers).toBe('function');
300
- expect(typeof sdk.directory.resolveUsers).toBe('function');
301
- });
302
-
303
- it('exposes the stable data surface (RFC 0002)', () => {
304
- expect(typeof sdk.data.query).toBe('function');
305
- expect(typeof sdk.data.provide).toBe('function');
306
- });
307
-
308
- it('exposes the activity surface (RFC 0005)', () => {
309
- expect(typeof sdk.activity.log).toBe('function');
310
- });
311
-
312
- it('exposes the portability surface (RFC 0007)', () => {
313
- expect(typeof sdk.portability.provideExport).toBe('function');
314
- expect(typeof sdk.portability.provideImport).toBe('function');
315
- });
316
-
317
- it('exposes the plugins discovery surface (RFC 0051)', () => {
318
- expect(typeof sdk.plugins.get).toBe('function');
319
- expect(typeof sdk.plugins.list).toBe('function');
320
- expect(typeof sdk.plugins.getConsentStatus).toBe('function');
321
- });
322
-
323
- it('exposes the env surface (RFC 0018)', () => {
324
- expect(typeof sdk.env.get).toBe('function');
325
- });
326
-
327
- it('exposes the experimental / reserved surface', () => {
328
- expect(typeof sdk.storage.put).toBe('function');
329
- expect(typeof sdk.storage.get).toBe('function');
330
- expect(typeof sdk.storage.delete).toBe('function');
331
- expect(typeof sdk.storage.list).toBe('function');
332
- expect(typeof sdk.storage.getSignedUrl).toBe('function');
333
- expect(typeof sdk.notifications.send).toBe('function');
334
- expect(typeof sdk.secrets.create).toBe('function');
335
- expect(typeof sdk.secrets.get).toBe('function');
336
- expect(typeof sdk.secrets.list).toBe('function');
337
- expect(typeof sdk.secrets.update).toBe('function');
338
- expect(typeof sdk.secrets.delete).toBe('function');
339
- expect(typeof sdk.connections.create).toBe('function');
340
- expect(typeof sdk.connections.list).toBe('function');
341
- expect(typeof sdk.connections.get).toBe('function');
342
- expect(typeof sdk.connections.update).toBe('function');
343
- expect(typeof sdk.connections.disconnect).toBe('function');
344
- expect(typeof sdk.connections.markUsed).toBe('function');
345
- expect(typeof sdk.connections.markError).toBe('function');
346
- expect(typeof sdk.connections.createOAuthState).toBe('function');
347
- expect(typeof sdk.connections.verifyOAuthState).toBe('function');
348
- expect(typeof sdk.connections.getProviderConfig).toBe('function');
349
- expect(typeof sdk.events.publish).toBe('function');
350
- expect(typeof sdk.events.subscribe).toBe('function');
351
- expect(typeof sdk.e2ee.getProfile).toBe('function');
352
- expect(typeof sdk.e2ee.createProfile).toBe('function');
353
- expect(typeof sdk.e2ee.getRecoveryWrapper).toBe('function');
354
- expect(typeof sdk.e2ee.setRecoveryWrapper).toBe('function');
355
- expect(typeof sdk.e2ee.enrollDevice).toBe('function');
356
- expect(typeof sdk.e2ee.listDevices).toBe('function');
357
- expect(typeof sdk.e2ee.revokeDevice).toBe('function');
358
- });
359
- });
360
-
361
- describe('sdk.db', () => {
362
- it('getClient delegates to the registered host', async () => {
363
- const client = await sdk.db.getClient();
364
- expect(client).toBe(mockDbClient);
365
- });
366
- });
367
-
368
- describe('sdk.platform', () => {
369
- it('getConfig delegates to the registered host', async () => {
370
- const config = await sdk.platform.getConfig();
371
- expect(config.tenantName).toBe('Test Workspace');
372
- expect(config.inviteOnly).toBe(false);
373
- expect(config.version).toBe('0.6.0');
374
- expect(config.instanceId).toBe('test-instance-uuid');
375
- });
376
- });
377
-
378
- describe('sdk — host guard', () => {
379
- it('requireHost throws when no host is registered', async () => {
380
- // Import requireHost directly to test the guard without a registered host.
381
- // We have a host registered in beforeAll, so we test the error message shape
382
- // by verifying our mock is returned (if it were null it would throw).
383
- const client = await sdk.db.getClient();
384
- expect(client).toBeDefined();
385
- });
386
- });
387
-
388
- describe('sdk — experimental surfaces throw NotImplementedError', () => {
389
- it('notifications.send delegates to the registered host (RFC 0015)', async () => {
390
- // No longer throws NotImplementedError — now delegates to the host.
391
- await expect(
392
- sdk.notifications.send({ recipientUserId: 'u1', title: 'Test' }),
393
- ).resolves.toBeUndefined();
394
- });
395
-
396
- it('events.publish / events.subscribe', () => {
397
- expect(() => sdk.events.publish('e', {})).toThrow(NotImplementedError);
398
- expect(() => sdk.events.subscribe('e', () => undefined)).toThrow(NotImplementedError);
399
- });
400
-
401
- it('data.provide delegates to the registered host (RFC 0002)', () => {
402
- const resolver = async () => [{ id: 1 }];
403
- sdk.data.provide('test-contract', resolver);
404
- expect(mockDataResolvers.get('test-contract')).toBe(resolver);
405
- });
406
-
407
- it('activity.log is implemented (RFC 0005)', () => {
408
- // activity.log delegates to the host via next/headers (requires request context);
409
- // we verify the method exists and is a function — runtime mediation is tested
410
- // indirectly via the host mock wired in beforeAll.
411
- expect(typeof sdk.activity.log).toBe('function');
412
- });
413
- });
414
-
415
- describe('sdk — error classes', () => {
416
- it('NotAuthenticatedError', () => {
417
- const err = new NotAuthenticatedError();
418
- expect(err.name).toBe('NotAuthenticatedError');
419
- expect(err).toBeInstanceOf(Error);
420
- });
421
-
422
- it('ConsentRequiredError', () => {
423
- const err = new ConsentRequiredError();
424
- expect(err.name).toBe('ConsentRequiredError');
425
- expect(err).toBeInstanceOf(Error);
426
- });
427
- });
@@ -1,63 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { markCurrentSessions, type RawSession } from '../sessions';
3
-
4
- function raw(token: string, createdAt: string, extra: Partial<RawSession> = {}): RawSession {
5
- return {
6
- token,
7
- createdAt,
8
- updatedAt: createdAt,
9
- expiresAt: '2026-12-31T00:00:00.000Z',
10
- ...extra,
11
- };
12
- }
13
-
14
- describe('markCurrentSessions', () => {
15
- it('flags the session whose token matches the current one', () => {
16
- const out = markCurrentSessions(
17
- [raw('a', '2026-06-01T00:00:00.000Z'), raw('b', '2026-06-02T00:00:00.000Z')],
18
- 'b',
19
- );
20
- expect(out.find((s) => s.token === 'b')?.current).toBe(true);
21
- expect(out.find((s) => s.token === 'a')?.current).toBe(false);
22
- });
23
-
24
- it('orders the current session first', () => {
25
- const out = markCurrentSessions(
26
- [raw('old', '2026-06-01T00:00:00.000Z'), raw('cur', '2026-05-01T00:00:00.000Z')],
27
- 'cur',
28
- );
29
- expect(out[0]?.token).toBe('cur');
30
- });
31
-
32
- it('orders the rest newest-first', () => {
33
- const out = markCurrentSessions(
34
- [
35
- raw('older', '2026-06-01T00:00:00.000Z'),
36
- raw('newer', '2026-06-03T00:00:00.000Z'),
37
- raw('cur', '2026-01-01T00:00:00.000Z'),
38
- ],
39
- 'cur',
40
- );
41
- expect(out.map((s) => s.token)).toEqual(['cur', 'newer', 'older']);
42
- });
43
-
44
- it('marks none current when the token is null', () => {
45
- const out = markCurrentSessions([raw('a', '2026-06-01T00:00:00.000Z')], null);
46
- expect(out.every((s) => !s.current)).toBe(true);
47
- });
48
-
49
- it('normalises missing userAgent/ipAddress to null', () => {
50
- const out = markCurrentSessions([raw('a', '2026-06-01T00:00:00.000Z')], 'a');
51
- expect(out[0]?.userAgent).toBeNull();
52
- expect(out[0]?.ipAddress).toBeNull();
53
- });
54
-
55
- it('passes through userAgent/ipAddress when present', () => {
56
- const out = markCurrentSessions(
57
- [raw('a', '2026-06-01T00:00:00.000Z', { userAgent: 'Firefox', ipAddress: '10.0.0.1' })],
58
- 'a',
59
- );
60
- expect(out[0]?.userAgent).toBe('Firefox');
61
- expect(out[0]?.ipAddress).toBe('10.0.0.1');
62
- });
63
- });
package/src/activity.ts DELETED
@@ -1,19 +0,0 @@
1
- import { headers } from 'next/headers';
2
- import { requireHost } from './host';
3
- import type { ActivityLogEntry } from './types';
4
-
5
- /**
6
- * Activity log (RFC 0005). Records a scoped audit event on behalf of the
7
- * current user; the runtime host injects the actor identity and plugin context
8
- * so plugins cannot forge them. Plugin-sourced events are always user-scoped.
9
- *
10
- * Requires the `activity:write` permission in the plugin manifest.
11
- */
12
- export const activity = {
13
- async log(entry: ActivityLogEntry): Promise<void> {
14
- const h = await headers();
15
- const actorId = h.get('x-sovereign-user-id');
16
- const pluginId = h.get('x-sovereign-plugin-id');
17
- await requireHost().activity.log(entry, actorId, pluginId);
18
- },
19
- };