@sovereignfs/sdk 1.25.0 → 2.0.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.
Files changed (55) hide show
  1. package/LICENSE +373 -661
  2. package/README.md +97 -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,75 +0,0 @@
1
- /**
2
- * Browser-only persistence for this device's local E2EE wrapping key
3
- * (RFC 0060). The key itself is generated non-extractable
4
- * (`e2ee-crypto.ts#generateDeviceKey`) so it must be persisted via
5
- * IndexedDB — the only Web storage API that can hold a live `CryptoKey`
6
- * across page loads (`localStorage`/cookies cannot serialize one).
7
- *
8
- * The device id is a random UUID, not sensitive on its own (it is just a
9
- * label distinguishing this browser/device's enrollment row from others) and
10
- * is stored in `localStorage` alongside the IndexedDB key entry.
11
- */
12
-
13
- const DB_NAME = 'sovereign-e2ee';
14
- const DB_VERSION = 1;
15
- const STORE_NAME = 'device-keys';
16
- const DEVICE_ID_STORAGE_KEY = 'sovereign:e2ee:deviceId';
17
-
18
- function openDb(): Promise<IDBDatabase> {
19
- return new Promise((resolve, reject) => {
20
- const request = indexedDB.open(DB_NAME, DB_VERSION);
21
- request.onupgradeneeded = () => {
22
- if (!request.result.objectStoreNames.contains(STORE_NAME)) {
23
- request.result.createObjectStore(STORE_NAME);
24
- }
25
- };
26
- request.onsuccess = () => resolve(request.result);
27
- request.onerror = () => reject(request.error ?? new Error('Failed to open IndexedDB.'));
28
- });
29
- }
30
-
31
- /** This browser/device's stable id — generated once, persisted in `localStorage`. */
32
- export function getOrCreateDeviceId(): string {
33
- const existing = localStorage.getItem(DEVICE_ID_STORAGE_KEY);
34
- if (existing) return existing;
35
- const id = crypto.randomUUID();
36
- localStorage.setItem(DEVICE_ID_STORAGE_KEY, id);
37
- return id;
38
- }
39
-
40
- /** Persist this device's local wrapping key, keyed by device id. */
41
- export async function storeDeviceKey(deviceId: string, key: CryptoKey): Promise<void> {
42
- const db = await openDb();
43
- await new Promise<void>((resolve, reject) => {
44
- const tx = db.transaction(STORE_NAME, 'readwrite');
45
- tx.objectStore(STORE_NAME).put(key, deviceId);
46
- tx.oncomplete = () => resolve();
47
- tx.onerror = () => reject(tx.error ?? new Error('Failed to store device key.'));
48
- });
49
- db.close();
50
- }
51
-
52
- /** Read this device's local wrapping key, or `null` if never enrolled on this device/browser. */
53
- export async function getDeviceKey(deviceId: string): Promise<CryptoKey | null> {
54
- const db = await openDb();
55
- const result = await new Promise<CryptoKey | null>((resolve, reject) => {
56
- const tx = db.transaction(STORE_NAME, 'readonly');
57
- const request = tx.objectStore(STORE_NAME).get(deviceId);
58
- request.onsuccess = () => resolve((request.result as CryptoKey | undefined) ?? null);
59
- request.onerror = () => reject(request.error ?? new Error('Failed to read device key.'));
60
- });
61
- db.close();
62
- return result;
63
- }
64
-
65
- /** Remove this device's local wrapping key (e.g. after the user revokes it from another device). */
66
- export async function forgetDeviceKey(deviceId: string): Promise<void> {
67
- const db = await openDb();
68
- await new Promise<void>((resolve, reject) => {
69
- const tx = db.transaction(STORE_NAME, 'readwrite');
70
- tx.objectStore(STORE_NAME).delete(deviceId);
71
- tx.oncomplete = () => resolve();
72
- tx.onerror = () => reject(tx.error ?? new Error('Failed to remove device key.'));
73
- });
74
- db.close();
75
- }
@@ -1,118 +0,0 @@
1
- /**
2
- * Encrypt/decrypt helpers for plugin object content under a per-object Data
3
- * Encryption Key (DEK) (RFC 0060 step 4). Pure WebCrypto, browser-only in
4
- * practice (also runs in Node for testing). Callers are responsible for
5
- * generating the DEK (`generateDek` in `e2ee-crypto.ts`) and wrapping it
6
- * under the unlocked CMK (`wrapDekWithCmk`) before storing the wrapped DEK
7
- * alongside the ciphertext this module produces — this module only ever
8
- * touches the unwrapped DEK, never the CMK.
9
- *
10
- * Two shapes are covered, matching RFC 0060's plaintext/encrypted metadata
11
- * split:
12
- * - `encryptBlob`/`decryptBlob` — binary payloads (document images, etc.)
13
- * meant to travel through `sdk.storage` as opaque ciphertext.
14
- * - `encryptJson`/`decryptJson` — the "encrypted metadata" half of an
15
- * object's fields (human-readable title, notes, document number, etc.),
16
- * as opposed to the plaintext routing metadata (object id, plugin id,
17
- * storage key) plugins keep server-visible.
18
- *
19
- * Each result carries its own IV and `algorithmVersion` so a future cipher
20
- * change is versioned per-object, not global — mirrors the wrapped-key shapes
21
- * in `e2ee-crypto.ts`.
22
- */
23
-
24
- const AES_GCM_IV_BYTES = 12;
25
-
26
- export const OBJECT_ALGORITHM_VERSION = 'v1';
27
-
28
- /** Encrypted binary payload — the `ciphertext` Blob is safe to upload as opaque bytes. */
29
- export interface EncryptedBlob {
30
- ciphertext: Blob;
31
- /** Base64url IV used for this encryption. */
32
- iv: string;
33
- algorithmVersion: string;
34
- /** The original `Blob.type`, needed to reconstruct it on decrypt. */
35
- contentType: string;
36
- }
37
-
38
- /** Encrypted JSON metadata — safe to store server-side as opaque ciphertext. */
39
- export interface EncryptedJson {
40
- /** Base64url ciphertext of the UTF-8 JSON-serialized value. */
41
- ciphertext: string;
42
- /** Base64url IV used for this encryption. */
43
- iv: string;
44
- algorithmVersion: string;
45
- }
46
-
47
- function toBase64Url(bytes: ArrayBuffer | Uint8Array): string {
48
- const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
49
- let binary = '';
50
- for (const byte of arr) binary += String.fromCharCode(byte);
51
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
52
- }
53
-
54
- function fromBase64Url(value: string): Uint8Array {
55
- const padded = value.replace(/-/g, '+').replace(/_/g, '/');
56
- const withPadding = padded + '='.repeat((4 - (padded.length % 4)) % 4);
57
- const binary = atob(withPadding);
58
- const bytes = new Uint8Array(binary.length);
59
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
60
- return bytes;
61
- }
62
-
63
- /** Encrypt a `Blob` (e.g. a document image) under the given DEK. */
64
- export async function encryptBlob(dek: CryptoKey, blob: Blob): Promise<EncryptedBlob> {
65
- const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));
66
- const plaintext = await blob.arrayBuffer();
67
- const ciphertext = await crypto.subtle.encrypt(
68
- { name: 'AES-GCM', iv: iv as BufferSource },
69
- dek,
70
- plaintext,
71
- );
72
- return {
73
- ciphertext: new Blob([ciphertext], { type: 'application/octet-stream' }),
74
- iv: toBase64Url(iv),
75
- algorithmVersion: OBJECT_ALGORITHM_VERSION,
76
- contentType: blob.type,
77
- };
78
- }
79
-
80
- /** Decrypt a `Blob` previously produced by `encryptBlob` with the matching DEK. */
81
- export async function decryptBlob(dek: CryptoKey, encrypted: EncryptedBlob): Promise<Blob> {
82
- const iv = fromBase64Url(encrypted.iv);
83
- const ciphertext = await encrypted.ciphertext.arrayBuffer();
84
- const plaintext = await crypto.subtle.decrypt(
85
- { name: 'AES-GCM', iv: iv as BufferSource },
86
- dek,
87
- ciphertext,
88
- );
89
- return new Blob([plaintext], { type: encrypted.contentType });
90
- }
91
-
92
- /** Encrypt a JSON-serializable value (e.g. an object's human-readable metadata) under the given DEK. */
93
- export async function encryptJson<T>(dek: CryptoKey, value: T): Promise<EncryptedJson> {
94
- const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));
95
- const plaintext = new TextEncoder().encode(JSON.stringify(value));
96
- const ciphertext = await crypto.subtle.encrypt(
97
- { name: 'AES-GCM', iv: iv as BufferSource },
98
- dek,
99
- plaintext,
100
- );
101
- return {
102
- ciphertext: toBase64Url(ciphertext),
103
- iv: toBase64Url(iv),
104
- algorithmVersion: OBJECT_ALGORITHM_VERSION,
105
- };
106
- }
107
-
108
- /** Decrypt a JSON value previously produced by `encryptJson` with the matching DEK. */
109
- export async function decryptJson<T>(dek: CryptoKey, encrypted: EncryptedJson): Promise<T> {
110
- const iv = fromBase64Url(encrypted.iv);
111
- const ciphertext = fromBase64Url(encrypted.ciphertext);
112
- const plaintext = await crypto.subtle.decrypt(
113
- { name: 'AES-GCM', iv: iv as BufferSource },
114
- dek,
115
- ciphertext as BufferSource,
116
- );
117
- return JSON.parse(new TextDecoder().decode(plaintext)) as T;
118
- }
package/src/e2ee-state.ts DELETED
@@ -1,72 +0,0 @@
1
- /**
2
- * Locked/unlocked state detection for client-side encryption (RFC 0060 SDK
3
- * responsibility: "returning normalized unsupported/locked/error states").
4
- * Consolidates the checks every consumer of `sdk.e2ee` needs before touching
5
- * encrypted data — browser support, this device's local wrapping key, and
6
- * whether this device has an active (non-revoked) enrollment — into one
7
- * reusable helper instead of each plugin (Account today, Wallet later)
8
- * reimplementing the same logic inline.
9
- */
10
- import type { E2eeDeviceEnrollment, E2eeProfile, E2eeState } from './types';
11
- import { getDeviceKey, getOrCreateDeviceId } from './e2ee-device';
12
-
13
- export interface E2eeLocalState {
14
- state: E2eeState;
15
- /** Null only when `state` is `'unsupported'` or `'not-set-up'`. */
16
- deviceId: string | null;
17
- /** This device's local wrapping key — present only when `state` is `'unlocked'`. */
18
- deviceKey: CryptoKey | null;
19
- /** This device's active enrollment record — present only when `state` is `'unlocked'`. */
20
- activeEnrollment: E2eeDeviceEnrollment | null;
21
- }
22
-
23
- /** True when the current environment has the WebCrypto and IndexedDB primitives E2EE needs. */
24
- export function browserSupportsE2ee(): boolean {
25
- return (
26
- typeof indexedDB !== 'undefined' &&
27
- typeof crypto !== 'undefined' &&
28
- typeof crypto.subtle !== 'undefined'
29
- );
30
- }
31
-
32
- /**
33
- * Pure classification given already-resolved inputs — no IndexedDB access,
34
- * so it's trivially unit-testable. `getE2eeLocalState` below is the
35
- * convenience wrapper that actually reads this device's local key.
36
- */
37
- export function classifyE2eeLocalState(
38
- profile: E2eeProfile | null,
39
- devices: E2eeDeviceEnrollment[],
40
- deviceId: string,
41
- deviceKey: CryptoKey | null,
42
- ): E2eeLocalState {
43
- if (!profile) {
44
- return { state: 'not-set-up', deviceId: null, deviceKey: null, activeEnrollment: null };
45
- }
46
- const activeEnrollment =
47
- devices.find((d) => d.deviceId === deviceId && d.revokedAt === null) ?? null;
48
- if (deviceKey && activeEnrollment) {
49
- return { state: 'unlocked', deviceId, deviceKey, activeEnrollment };
50
- }
51
- return { state: 'locked', deviceId, deviceKey: null, activeEnrollment: null };
52
- }
53
-
54
- /**
55
- * Resolve this device's current E2EE state against the user's profile and
56
- * enrollment list (typically loaded server-side and passed in by the
57
- * caller). Reads this device's local wrapping key from IndexedDB.
58
- */
59
- export async function getE2eeLocalState(
60
- profile: E2eeProfile | null,
61
- devices: E2eeDeviceEnrollment[],
62
- ): Promise<E2eeLocalState> {
63
- if (!browserSupportsE2ee()) {
64
- return { state: 'unsupported', deviceId: null, deviceKey: null, activeEnrollment: null };
65
- }
66
- if (!profile) {
67
- return { state: 'not-set-up', deviceId: null, deviceKey: null, activeEnrollment: null };
68
- }
69
- const deviceId = getOrCreateDeviceId();
70
- const deviceKey = await getDeviceKey(deviceId);
71
- return classifyE2eeLocalState(profile, devices, deviceId, deviceKey);
72
- }
package/src/e2ee.ts DELETED
@@ -1,65 +0,0 @@
1
- import { headers } from 'next/headers';
2
- import { NotAuthenticatedError } from './errors';
3
- import { requireHost } from './host';
4
- import type {
5
- CreateE2eeProfileInput,
6
- E2eeContext,
7
- E2eeDeviceEnrollment,
8
- E2eeProfile,
9
- E2eeRecoveryWrapper,
10
- EnrollE2eeDeviceInput,
11
- SetE2eeRecoveryWrapperInput,
12
- } from './types';
13
-
14
- const DEFAULT_TENANT_ID = 'default';
15
-
16
- async function e2eeContext(): Promise<E2eeContext> {
17
- const h = await headers();
18
- const userId = h.get('x-sovereign-user-id');
19
- if (!userId) throw new NotAuthenticatedError();
20
- return { tenantId: DEFAULT_TENANT_ID, userId };
21
- }
22
-
23
- /**
24
- * Client-side encryption profile persistence (RFC 0060). Server-side plumbing
25
- * only — the CMK is generated and wrapped/unwrapped in the browser
26
- * (`@sovereignfs/sdk/e2ee-crypto`, `@sovereignfs/sdk/e2ee-device`) before any
27
- * of these methods are called; the server only ever stores/returns opaque
28
- * ciphertext and non-sensitive KDF/algorithm metadata.
29
- */
30
- export const e2ee = {
31
- async getProfile(): Promise<E2eeProfile | null> {
32
- const context = await e2eeContext();
33
- return requireHost().e2ee.getProfile(context);
34
- },
35
-
36
- async createProfile(input: CreateE2eeProfileInput): Promise<E2eeProfile> {
37
- const context = await e2eeContext();
38
- return requireHost().e2ee.createProfile(input, context);
39
- },
40
-
41
- async getRecoveryWrapper(): Promise<E2eeRecoveryWrapper | null> {
42
- const context = await e2eeContext();
43
- return requireHost().e2ee.getRecoveryWrapper(context);
44
- },
45
-
46
- async setRecoveryWrapper(input: SetE2eeRecoveryWrapperInput): Promise<E2eeRecoveryWrapper> {
47
- const context = await e2eeContext();
48
- return requireHost().e2ee.setRecoveryWrapper(input, context);
49
- },
50
-
51
- async enrollDevice(input: EnrollE2eeDeviceInput): Promise<E2eeDeviceEnrollment> {
52
- const context = await e2eeContext();
53
- return requireHost().e2ee.enrollDevice(input, context);
54
- },
55
-
56
- async listDevices(): Promise<E2eeDeviceEnrollment[]> {
57
- const context = await e2eeContext();
58
- return requireHost().e2ee.listDevices(context);
59
- },
60
-
61
- async revokeDevice(id: string): Promise<void> {
62
- const context = await e2eeContext();
63
- return requireHost().e2ee.revokeDevice(id, context);
64
- },
65
- };
package/src/env.ts DELETED
@@ -1,37 +0,0 @@
1
- import { headers } from 'next/headers';
2
-
3
- /**
4
- * Plugin-scoped environment variables (RFC 0018).
5
- *
6
- * `sdk.env.get(key)` reads the calling plugin's `SV_PLUGIN_<SLUG>_<KEY>`
7
- * environment variable, identified by the `x-sovereign-plugin-id` header
8
- * that the runtime middleware injects on every plugin route request. Returns
9
- * `null` when the variable is absent or the call is made outside a plugin
10
- * route context (no `x-sovereign-plugin-id` header).
11
- *
12
- * Declare the variable in the manifest `env` field:
13
- * ```json
14
- * "env": {
15
- * "API_KEY": { "description": "Third-party API key", "secret": true, "scope": "runtime", "required": true }
16
- * }
17
- * ```
18
- *
19
- * Then read it in a Server Component or Route Handler:
20
- * ```ts
21
- * import { sdk } from '@sovereignfs/sdk';
22
- * const apiKey = await sdk.env.get('API_KEY');
23
- * ```
24
- *
25
- * For `scope: "build"` variables (NEXT_PUBLIC_SV_PLUGIN_*), read the env var
26
- * directly from `process.env` in client components — Next.js inlines NEXT_PUBLIC_*
27
- * vars at build time and sdk.env.get is server-only (it uses next/headers).
28
- */
29
- export const env = {
30
- async get(key: string): Promise<string | null> {
31
- const h = await headers();
32
- const pluginId = h.get('x-sovereign-plugin-id');
33
- if (!pluginId) return null;
34
- const slug = pluginId.replace(/[.-]/g, '_').toUpperCase();
35
- return process.env[`SV_PLUGIN_${slug}_${key}`] ?? null;
36
- },
37
- };
package/src/errors.ts DELETED
@@ -1,46 +0,0 @@
1
- /**
2
- * Thrown by SDK surfaces that have no standalone implementation: the v1 methods
3
- * that the Sovereign runtime supplies at runtime, and the post-v1 surfaces
4
- * (storage, notifications, events) that are declared but not yet implemented.
5
- */
6
- export class NotImplementedError extends Error {
7
- constructor(message: string) {
8
- super(message);
9
- this.name = 'NotImplementedError';
10
- }
11
- }
12
-
13
- /** Thrown by `sdk.auth.requireSession()` when no authenticated session is present. */
14
- export class NotAuthenticatedError extends Error {
15
- constructor() {
16
- super('No authenticated session. The caller must be within an authenticated request.');
17
- this.name = 'NotAuthenticatedError';
18
- }
19
- }
20
-
21
- /**
22
- * Thrown by `sdk.data.query()` when the current user has not granted the calling
23
- * (consumer) plugin consent to read the requested provider contract — the
24
- * consent-gated cross-plugin data-sharing mechanism (RFC 0002). Reserved
25
- * alongside that surface; not raised until the mechanism is implemented.
26
- */
27
- export class ConsentRequiredError extends Error {
28
- constructor(message = 'User consent is required to access this plugin data.') {
29
- super(message);
30
- this.name = 'ConsentRequiredError';
31
- }
32
- }
33
-
34
- /**
35
- * Thrown by `sdk.billing.requireEntitlement()` when the current user has no
36
- * valid (active, unexpired) entitlement for the calling plugin (RFC 0003).
37
- * The runtime middleware also enforces this at the route level, so most plugins
38
- * never need to call `requireEntitlement()` directly — it is for in-plugin
39
- * feature gating by tier.
40
- */
41
- export class EntitlementRequiredError extends Error {
42
- constructor(message = 'An active entitlement is required to access this feature.') {
43
- super(message);
44
- this.name = 'EntitlementRequiredError';
45
- }
46
- }
package/src/host.ts DELETED
@@ -1,251 +0,0 @@
1
- import type { DataContractRef, DataContractResolver } from './data';
2
- import type { DeletionHandler, ExportResolver, ImportHandler } from './portability';
3
- import type { ConsentStatus, PluginAvailability, PluginListFilter } from './plugins';
4
- import type {
5
- ActivityLogEntry,
6
- DirectoryUser,
7
- DrizzleClient,
8
- MailOptions,
9
- PlatformConfig,
10
- ProviderConfig,
11
- ResolveUsersInput,
12
- SearchUsersInput,
13
- SendNotificationInput,
14
- CreateSecretInput,
15
- ConnectionContext,
16
- ConnectionListFilter,
17
- ConnectionOAuthState,
18
- ConnectionRef,
19
- CreateConnectionInput,
20
- CreateE2eeProfileInput,
21
- E2eeContext,
22
- E2eeDeviceEnrollment,
23
- E2eeProfile,
24
- E2eeRecoveryWrapper,
25
- EnrollE2eeDeviceInput,
26
- MarkConnectionErrorInput,
27
- OAuthStateInput,
28
- SecretContext,
29
- SecretRef,
30
- SecretScope,
31
- SetE2eeRecoveryWrapperInput,
32
- StorageContext,
33
- StorageObject,
34
- StoragePutInput,
35
- UpdateConnectionInput,
36
- } from './types';
37
-
38
- /**
39
- * The host interface that the Sovereign runtime provides to the SDK.
40
- * Every method that touches platform infrastructure (DB, mailer, config) is
41
- * routed through this interface — the SDK itself has no runtime dependencies.
42
- */
43
- export interface SdkHost {
44
- db: {
45
- /**
46
- * `pluginId` is the calling plugin's manifest id, read from the
47
- * `x-sovereign-plugin-id` request header by the SDK. `null` means the call
48
- * happened outside a plugin route context; the platform DB is returned.
49
- */
50
- getClient(pluginId: string | null): Promise<DrizzleClient>;
51
- };
52
- mailer: {
53
- send(options: MailOptions): Promise<void>;
54
- };
55
- platform: {
56
- getConfig(): Promise<PlatformConfig>;
57
- };
58
- directory: {
59
- /**
60
- * Search active users in the current tenant. `requestingUserId` and
61
- * `tenantId` come from runtime-injected request context, not plugin input.
62
- */
63
- searchUsers(
64
- input: SearchUsersInput,
65
- requestingUserId: string,
66
- tenantId: string,
67
- ): Promise<DirectoryUser[]>;
68
- /**
69
- * Resolve explicit user IDs to display-safe profile fields for active users
70
- * in the current tenant.
71
- */
72
- resolveUsers(
73
- input: ResolveUsersInput,
74
- requestingUserId: string,
75
- tenantId: string,
76
- ): Promise<DirectoryUser[]>;
77
- };
78
- data: {
79
- /** Register a resolver for a contract this plugin provides. */
80
- provide(contract: string, resolver: DataContractResolver): void;
81
- /**
82
- * Query a provider plugin's contract for the current user.
83
- * `consumerId` and `userId` are read from request headers by the SDK caller.
84
- */
85
- query(
86
- ref: DataContractRef,
87
- consumerId: string | null,
88
- userId: string | null,
89
- tenantId: string,
90
- params: unknown,
91
- ): Promise<unknown[]>;
92
- };
93
- activity: {
94
- /**
95
- * Record one activity event.
96
- * `actorId`, `pluginId`, and `tenantId` are injected by the runtime host
97
- * from request headers so plugins cannot forge actor identity.
98
- */
99
- log(entry: ActivityLogEntry, actorId: string | null, pluginId: string | null): Promise<void>;
100
- };
101
- portability: {
102
- /**
103
- * Register a plugin's export resolver, keyed by `pluginId` (resolved by the
104
- * SDK from the request context). The runtime invokes it at export time.
105
- */
106
- provideExport(pluginId: string, resolver: ExportResolver): void;
107
- /** Register a plugin's import handler, keyed by `pluginId`. */
108
- provideImport(pluginId: string, handler: ImportHandler): void;
109
- /** Register a plugin's deletion handler (RFC 0033), keyed by `pluginId`. */
110
- provideDelete(pluginId: string, handler: DeletionHandler): void;
111
- };
112
- plugins: {
113
- /** Discover one installed plugin's status (RFC 0051), scoped to the given user. */
114
- get(
115
- id: string,
116
- userId: string | null,
117
- capabilities: readonly string[],
118
- ): Promise<PluginAvailability | null>;
119
- /** Discover installed plugins, optionally filtered, scoped to the given user. */
120
- list(
121
- filter: PluginListFilter | undefined,
122
- userId: string | null,
123
- capabilities: readonly string[],
124
- ): Promise<PluginAvailability[]>;
125
- /** Whether `userId` has an active consent grant for `ref`, requested by `consumerId`. */
126
- getConsentStatus(
127
- ref: DataContractRef,
128
- consumerId: string,
129
- userId: string,
130
- ): Promise<ConsentStatus>;
131
- };
132
- notifications: {
133
- /**
134
- * Deliver one notification to a user. The runtime injects `source`,
135
- * `sourceType`, and `tenantId` from the request context so the plugin only
136
- * supplies the payload fields.
137
- */
138
- send(input: SendNotificationInput, pluginId: string): Promise<void>;
139
- };
140
- storage: {
141
- put(input: StoragePutInput, context: StorageContext): Promise<StorageObject>;
142
- get(
143
- key: string,
144
- context: StorageContext,
145
- ): Promise<(StorageObject & { body: ReadableStream }) | null>;
146
- delete(key: string, context: StorageContext): Promise<void>;
147
- list(prefix: string | undefined, context: StorageContext): Promise<StorageObject[]>;
148
- getSignedUrl(
149
- key: string,
150
- options: { expiresInSeconds?: number } | undefined,
151
- context: StorageContext,
152
- ): Promise<string>;
153
- };
154
- /**
155
- * Client-side encryption profile persistence (RFC 0060). Pure metadata
156
- * plumbing — no encryption happens here; the CMK is generated and
157
- * wrapped/unwrapped entirely in the browser (`@sovereignfs/sdk`'s
158
- * `e2ee-crypto`/`e2ee-device` modules) before these methods are ever
159
- * called with the resulting ciphertext.
160
- */
161
- e2ee: {
162
- getProfile(context: E2eeContext): Promise<E2eeProfile | null>;
163
- createProfile(input: CreateE2eeProfileInput, context: E2eeContext): Promise<E2eeProfile>;
164
- getRecoveryWrapper(context: E2eeContext): Promise<E2eeRecoveryWrapper | null>;
165
- setRecoveryWrapper(
166
- input: SetE2eeRecoveryWrapperInput,
167
- context: E2eeContext,
168
- ): Promise<E2eeRecoveryWrapper>;
169
- enrollDevice(input: EnrollE2eeDeviceInput, context: E2eeContext): Promise<E2eeDeviceEnrollment>;
170
- listDevices(context: E2eeContext): Promise<E2eeDeviceEnrollment[]>;
171
- revokeDevice(id: string, context: E2eeContext): Promise<void>;
172
- };
173
- secrets: {
174
- create(input: CreateSecretInput, context: SecretContext): Promise<SecretRef>;
175
- get(id: string, context: SecretContext): Promise<string | null>;
176
- list(scope: SecretScope | undefined, context: SecretContext): Promise<SecretRef[]>;
177
- update(id: string, value: string, context: SecretContext): Promise<SecretRef>;
178
- delete(id: string, context: SecretContext): Promise<void>;
179
- };
180
- connections: {
181
- create(input: CreateConnectionInput, context: ConnectionContext): Promise<ConnectionRef>;
182
- list(
183
- filter: ConnectionListFilter | undefined,
184
- context: ConnectionContext,
185
- ): Promise<ConnectionRef[]>;
186
- get(id: string, context: ConnectionContext): Promise<ConnectionRef | null>;
187
- update(
188
- id: string,
189
- input: UpdateConnectionInput,
190
- context: ConnectionContext,
191
- ): Promise<ConnectionRef>;
192
- disconnect(id: string, context: ConnectionContext): Promise<void>;
193
- markUsed(id: string, context: ConnectionContext): Promise<void>;
194
- markError(
195
- id: string,
196
- input: MarkConnectionErrorInput,
197
- context: ConnectionContext,
198
- ): Promise<ConnectionRef>;
199
- createOAuthState(input: OAuthStateInput, context: ConnectionContext): Promise<string>;
200
- verifyOAuthState(state: string, context: ConnectionContext): Promise<ConnectionOAuthState>;
201
- getProviderConfig(provider: string, context: ConnectionContext): Promise<ProviderConfig>;
202
- };
203
- }
204
-
205
- /**
206
- * The host is stored on `globalThis` under a `Symbol.for` key, NOT a plain
207
- * module-level variable. Next.js compiles instrumentation, route handlers, and
208
- * server actions into separate bundles, each of which can get its own instance
209
- * of this module — and dev HMR re-evaluates it on edits, resetting any
210
- * module-level state. With a per-module `let`, `provideHost()` (called once from
211
- * `runtime/instrumentation.ts`) would set the host on one instance while a
212
- * plugin server action reads `null` from another, throwing "no runtime host".
213
- * A `Symbol.for`-keyed global is shared across every module instance in the same
214
- * Node process, so the single registration is always visible.
215
- */
216
- const HOST_KEY = Symbol.for('@sovereignfs/sdk:host');
217
-
218
- interface HostHolder {
219
- [HOST_KEY]?: SdkHost | null;
220
- }
221
-
222
- function holder(): HostHolder {
223
- return globalThis as unknown as HostHolder;
224
- }
225
-
226
- /**
227
- * Register the platform host implementation. Called once at runtime startup
228
- * (via `runtime/instrumentation.ts`) before any request is served.
229
- *
230
- * Exported from `@sovereignfs/sdk` so the runtime can call it; plugin code
231
- * should never need to call this.
232
- */
233
- export function provideHost(host: SdkHost): void {
234
- holder()[HOST_KEY] = host;
235
- }
236
-
237
- /**
238
- * Return the registered host implementation, throwing if none is registered
239
- * (i.e. the SDK is being executed outside the Sovereign runtime).
240
- */
241
- export function requireHost(): SdkHost {
242
- const host = holder()[HOST_KEY];
243
- if (!host) {
244
- throw new Error(
245
- '@sovereignfs/sdk: no runtime host is registered. ' +
246
- 'SDK methods run inside the Sovereign runtime — start the platform with `pnpm dev` ' +
247
- 'or `pnpm sv dev` and ensure the plugin is installed.',
248
- );
249
- }
250
- return host;
251
- }