@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.
- package/LICENSE +373 -661
- package/README.md +97 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +4 -0
- package/dist/chunk-NYL4LLXF.js +65 -0
- package/dist/index.d.ts +57 -529
- package/dist/index.js +1283 -572
- package/dist/next.d.ts +37 -0
- package/dist/next.js +68 -0
- package/dist/session-DJl2XYMc.d.ts +44 -0
- package/dist/sovereign-CazGKEVc.d.ts +517 -0
- package/dist/testing.d.ts +181 -0
- package/dist/testing.js +560 -0
- package/package.json +53 -27
- package/dist/chunk-MLKGABMK.js +0 -9
- package/dist/chunk-NQLGPAL6.js +0 -62
- package/dist/e2ee-crypto.d.ts +0 -84
- package/dist/e2ee-crypto.js +0 -162
- package/dist/e2ee-device.d.ts +0 -21
- package/dist/e2ee-device.js +0 -13
- package/dist/e2ee-object.d.ts +0 -50
- package/dist/e2ee-object.js +0 -75
- package/dist/e2ee-state-U-GEJ7oL.d.ts +0 -411
- package/dist/e2ee-state.d.ts +0 -1
- package/dist/e2ee-state.js +0 -36
- package/src/__tests__/e2ee-crypto.test.ts +0 -156
- package/src/__tests__/e2ee-object.test.ts +0 -90
- package/src/__tests__/e2ee-state.test.ts +0 -83
- package/src/__tests__/sdk.test.ts +0 -427
- package/src/__tests__/sessions.test.ts +0 -63
- package/src/activity.ts +0 -19
- package/src/auth.ts +0 -134
- package/src/connections.ts +0 -106
- package/src/data.ts +0 -88
- package/src/db.ts +0 -34
- package/src/directory.ts +0 -31
- package/src/e2ee-crypto.ts +0 -260
- package/src/e2ee-device.ts +0 -75
- package/src/e2ee-object.ts +0 -118
- package/src/e2ee-state.ts +0 -72
- package/src/e2ee.ts +0 -65
- package/src/env.ts +0 -37
- package/src/errors.ts +0 -46
- package/src/host.ts +0 -251
- package/src/index.ts +0 -160
- package/src/mailer.ts +0 -7
- package/src/notifications.ts +0 -36
- package/src/platform.ts +0 -10
- package/src/plugins.ts +0 -146
- package/src/portability.ts +0 -176
- package/src/secrets.ts +0 -68
- package/src/sessions.ts +0 -36
- package/src/storage.ts +0 -49
- package/src/types.ts +0 -418
- package/src/unimplemented.ts +0 -48
package/src/index.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import * as auth from './auth';
|
|
2
|
-
import { activity } from './activity';
|
|
3
|
-
import { connections } from './connections';
|
|
4
|
-
import { data } from './data';
|
|
5
|
-
import { directory } from './directory';
|
|
6
|
-
import * as db from './db';
|
|
7
|
-
import { e2ee } from './e2ee';
|
|
8
|
-
import { env } from './env';
|
|
9
|
-
import * as mailer from './mailer';
|
|
10
|
-
import { notifications } from './notifications';
|
|
11
|
-
import * as platform from './platform';
|
|
12
|
-
import { plugins } from './plugins';
|
|
13
|
-
import { portability } from './portability';
|
|
14
|
-
import { secrets } from './secrets';
|
|
15
|
-
import { storage } from './storage';
|
|
16
|
-
import { billing, events } from './unimplemented';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The Sovereign SDK — the only contract between a plugin and the platform.
|
|
20
|
-
*
|
|
21
|
-
* **Stable surface (covered by the v1.0.0 semver guarantee** — see
|
|
22
|
-
* `docs/sdk-stability.md`): `auth`, `db`, `mailer`, `platform`. These are
|
|
23
|
-
* implemented and will not change in breaking ways without a major bump +
|
|
24
|
-
* migration note (NFR-04).
|
|
25
|
-
*
|
|
26
|
-
* **Experimental / reserved (NOT covered by the stability guarantee):** `data`
|
|
27
|
-
* (cross-plugin data sharing, RFC 0002), `activity` (activity log, RFC 0005),
|
|
28
|
-
* `portability` (user data export/import, RFC 0007), `env` (plugin-scoped env
|
|
29
|
-
* vars, RFC 0018), `notifications` (notification center, RFC 0015),
|
|
30
|
-
* `directory` (member selection, RFC 0041), `secrets` (plugin secret vault,
|
|
31
|
-
* RFC 0043), `storage` (plugin file storage, RFC 0044), `connections`
|
|
32
|
-
* (external provider connections, RFC 0049), `e2ee` (client-side encryption
|
|
33
|
-
* profile persistence, RFC 0060 — profile/device plumbing only; see
|
|
34
|
-
* `e2ee-crypto`/`e2ee-device`/`e2ee-object`/`e2ee-state` for the browser-only
|
|
35
|
-
* crypto and state helpers that produce the ciphertext these methods store),
|
|
36
|
-
* `plugins` (dependency discovery and cross-plugin references, RFC 0051),
|
|
37
|
-
* `billing` (plugin monetization / entitlement gating, RFC 0003), `events`.
|
|
38
|
-
* `data`, `activity`, `portability`, `env`, `notifications`, `directory`,
|
|
39
|
-
* `secrets`, `storage`, `connections`, `e2ee`, and `plugins` are implemented;
|
|
40
|
-
* `billing` and `events` throw `NotImplementedError` until their backing
|
|
41
|
-
* mechanisms ship. Their shape may change before they stabilise.
|
|
42
|
-
*/
|
|
43
|
-
export const sdk = {
|
|
44
|
-
// Stable (v1.0.0).
|
|
45
|
-
auth,
|
|
46
|
-
db,
|
|
47
|
-
mailer,
|
|
48
|
-
platform,
|
|
49
|
-
// Experimental / reserved — shape may change; some throw until implemented.
|
|
50
|
-
data,
|
|
51
|
-
storage,
|
|
52
|
-
directory,
|
|
53
|
-
notifications,
|
|
54
|
-
secrets,
|
|
55
|
-
connections,
|
|
56
|
-
e2ee,
|
|
57
|
-
events,
|
|
58
|
-
activity,
|
|
59
|
-
portability,
|
|
60
|
-
plugins,
|
|
61
|
-
env,
|
|
62
|
-
billing,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export { provideHost } from './host';
|
|
66
|
-
export type { SdkHost } from './host';
|
|
67
|
-
/**
|
|
68
|
-
* Client-side CMK/DEK crypto and state helpers (RFC 0060) — browser-only
|
|
69
|
-
* (WebCrypto/IndexedDB). Deliberately NOT re-exported here: this barrel also
|
|
70
|
-
* reaches server-only modules (e.g. `activity.ts`'s `next/headers` import),
|
|
71
|
-
* and Next.js's client/server boundary check flags the whole module graph
|
|
72
|
-
* reachable from an import — not just the specific named export used — so a
|
|
73
|
-
* `'use client'` component importing anything from `@sovereignfs/sdk`
|
|
74
|
-
* directly would fail to build. Import from the dedicated subpaths instead:
|
|
75
|
-
*
|
|
76
|
-
* ```ts
|
|
77
|
-
* import { generateCmk, wrapCmkWithRecoverySecret } from '@sovereignfs/sdk/e2ee-crypto';
|
|
78
|
-
* import { getOrCreateDeviceId, storeDeviceKey } from '@sovereignfs/sdk/e2ee-device';
|
|
79
|
-
* import { encryptBlob, encryptJson } from '@sovereignfs/sdk/e2ee-object';
|
|
80
|
-
* import { getE2eeLocalState } from '@sovereignfs/sdk/e2ee-state';
|
|
81
|
-
* ```
|
|
82
|
-
*
|
|
83
|
-
* Types are erased at compile time (no runtime module graph), so they stay
|
|
84
|
-
* exported from the main barrel below for convenience.
|
|
85
|
-
*/
|
|
86
|
-
export type { RecoveryWrappedCmk, WrappedCmk, WrappedDek } from './e2ee-crypto';
|
|
87
|
-
export type { EncryptedBlob, EncryptedJson } from './e2ee-object';
|
|
88
|
-
export type { E2eeLocalState } from './e2ee-state';
|
|
89
|
-
export {
|
|
90
|
-
NotImplementedError,
|
|
91
|
-
NotAuthenticatedError,
|
|
92
|
-
ConsentRequiredError,
|
|
93
|
-
EntitlementRequiredError,
|
|
94
|
-
} from './errors';
|
|
95
|
-
export type { DataContractRef, DataContractResolver } from './data';
|
|
96
|
-
export type {
|
|
97
|
-
ExportContext,
|
|
98
|
-
ExportOptions,
|
|
99
|
-
ImportContext,
|
|
100
|
-
PluginExportSection,
|
|
101
|
-
ExportSecretMetadata,
|
|
102
|
-
ExportResolver,
|
|
103
|
-
ImportHandler,
|
|
104
|
-
DeletionContext,
|
|
105
|
-
DeletionResult,
|
|
106
|
-
DeletionHandler,
|
|
107
|
-
} from './portability';
|
|
108
|
-
export type {
|
|
109
|
-
PluginAvailability,
|
|
110
|
-
PluginContractSummary,
|
|
111
|
-
PluginListFilter,
|
|
112
|
-
ConsentStatus,
|
|
113
|
-
PluginReference,
|
|
114
|
-
} from './plugins';
|
|
115
|
-
export type {
|
|
116
|
-
Session,
|
|
117
|
-
SessionUser,
|
|
118
|
-
ActiveSession,
|
|
119
|
-
ChangePasswordInput,
|
|
120
|
-
DirectoryUser,
|
|
121
|
-
MailOptions,
|
|
122
|
-
PlatformConfig,
|
|
123
|
-
ProviderConfig,
|
|
124
|
-
ProviderConfigSource,
|
|
125
|
-
ResolveUsersInput,
|
|
126
|
-
SearchUsersInput,
|
|
127
|
-
ActivityLogEntry,
|
|
128
|
-
DrizzleClient,
|
|
129
|
-
SendNotificationInput,
|
|
130
|
-
ConnectionContext,
|
|
131
|
-
ConnectionListFilter,
|
|
132
|
-
ConnectionOAuthState,
|
|
133
|
-
ConnectionRef,
|
|
134
|
-
ConnectionScope,
|
|
135
|
-
ConnectionStatus,
|
|
136
|
-
CreateConnectionInput,
|
|
137
|
-
MarkConnectionErrorInput,
|
|
138
|
-
OAuthStateInput,
|
|
139
|
-
SanitizedConnectionError,
|
|
140
|
-
CreateSecretInput,
|
|
141
|
-
ScheduleContext,
|
|
142
|
-
ScheduleHandler,
|
|
143
|
-
SecretContext,
|
|
144
|
-
SecretRef,
|
|
145
|
-
SecretScope,
|
|
146
|
-
StorageContext,
|
|
147
|
-
StorageObject,
|
|
148
|
-
StoragePutInput,
|
|
149
|
-
E2eeProfile,
|
|
150
|
-
E2eeProfileStatus,
|
|
151
|
-
E2eeRecoveryWrapper,
|
|
152
|
-
E2eeDeviceEnrollment,
|
|
153
|
-
E2eeState,
|
|
154
|
-
E2eeContext,
|
|
155
|
-
CreateE2eeProfileInput,
|
|
156
|
-
SetE2eeRecoveryWrapperInput,
|
|
157
|
-
EnrollE2eeDeviceInput,
|
|
158
|
-
UpdateSecretInput,
|
|
159
|
-
UpdateConnectionInput,
|
|
160
|
-
} from './types';
|
package/src/mailer.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { requireHost } from './host';
|
|
2
|
-
import type { MailOptions } from './types';
|
|
3
|
-
|
|
4
|
-
/** Sends an email via the platform mailer. No-ops when SMTP is not configured. */
|
|
5
|
-
export async function send(options: MailOptions): Promise<void> {
|
|
6
|
-
await requireHost().mailer.send(options);
|
|
7
|
-
}
|
package/src/notifications.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { requireHost } from './host';
|
|
2
|
-
import type { SendNotificationInput } from './types';
|
|
3
|
-
|
|
4
|
-
function getPluginId(headers: Headers): string | null {
|
|
5
|
-
return headers.get('x-sovereign-plugin-id');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Notification Center SDK surface (RFC 0015).
|
|
10
|
-
*
|
|
11
|
-
* Requires the `notifications:send` manifest permission.
|
|
12
|
-
* The runtime injects source identity from the request context.
|
|
13
|
-
*/
|
|
14
|
-
export const notifications = {
|
|
15
|
-
/**
|
|
16
|
-
* Send a notification to a user.
|
|
17
|
-
*
|
|
18
|
-
* The `source` and `sourceType` fields are set by the runtime from the
|
|
19
|
-
* calling plugin's ID (injected via `x-sovereign-plugin-id` header) — plugins
|
|
20
|
-
* cannot forge sender identity.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```ts
|
|
24
|
-
* await sdk.notifications.send({
|
|
25
|
-
* recipientUserId: userId,
|
|
26
|
-
* title: 'Your export is ready',
|
|
27
|
-
* url: '/myPlugin/exports',
|
|
28
|
-
* category: 'info',
|
|
29
|
-
* });
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
async send(input: SendNotificationInput, requestHeaders?: Headers): Promise<void> {
|
|
33
|
-
const pluginId = requestHeaders ? (getPluginId(requestHeaders) ?? 'unknown') : 'unknown';
|
|
34
|
-
return requireHost().notifications.send(input, pluginId);
|
|
35
|
-
},
|
|
36
|
-
};
|
package/src/platform.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { requireHost } from './host';
|
|
2
|
-
import type { PlatformConfig } from './types';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Returns the platform runtime configuration (SRS PLT-06). Async by contract:
|
|
6
|
-
* the platform DB is dialect-agnostic and Postgres has no synchronous query.
|
|
7
|
-
*/
|
|
8
|
-
export async function getConfig(): Promise<PlatformConfig> {
|
|
9
|
-
return requireHost().platform.getConfig();
|
|
10
|
-
}
|
package/src/plugins.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { headers } from 'next/headers';
|
|
2
|
-
import type { DataContractRef } from './data';
|
|
3
|
-
import { requireHost } from './host';
|
|
4
|
-
|
|
5
|
-
function parseCapabilities(raw: string | null): readonly string[] {
|
|
6
|
-
if (!raw) return [];
|
|
7
|
-
try {
|
|
8
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
9
|
-
return Array.isArray(parsed)
|
|
10
|
-
? parsed.filter((cap): cap is string => typeof cap === 'string')
|
|
11
|
-
: [];
|
|
12
|
-
} catch {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** A data contract a plugin declares in its manifest `data.provides`. */
|
|
18
|
-
export interface PluginContractSummary {
|
|
19
|
-
contract: string;
|
|
20
|
-
version: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Discovery status of an installed plugin, scoped to the requesting user
|
|
25
|
-
* (RFC 0051). Never exposes private provider data — only install/enable
|
|
26
|
-
* status, launch availability, and declared (not consented) contracts.
|
|
27
|
-
*/
|
|
28
|
-
export interface PluginAvailability {
|
|
29
|
-
id: string;
|
|
30
|
-
name: string;
|
|
31
|
-
routePrefix: string;
|
|
32
|
-
icon?: string;
|
|
33
|
-
/** Always `true` — `get()`/`list()` only ever return installed plugins. */
|
|
34
|
-
installed: true;
|
|
35
|
-
/** Whether the plugin is enabled instance-wide (Console toggle / example default). */
|
|
36
|
-
enabled: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Whether the *current* user could launch this plugin right now: enabled,
|
|
39
|
-
* not blocked by an `adminOnly` gate the user lacks `console:access` for,
|
|
40
|
-
* and not paywalled by an unmet entitlement (RFC 0003). `false` when called
|
|
41
|
-
* without an authenticated user in context.
|
|
42
|
-
*/
|
|
43
|
-
availableToUser: boolean;
|
|
44
|
-
/** Data contracts this plugin exposes (`data.provides`), if any. */
|
|
45
|
-
providesContracts: PluginContractSummary[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface PluginListFilter {
|
|
49
|
-
/** Only return plugins that declare this contract name in `data.provides`. */
|
|
50
|
-
providesContract?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Whether the current user has an active consent grant for a (consumer,
|
|
55
|
-
* provider, contract, version) tuple. `'not_granted'` covers both "never
|
|
56
|
-
* granted" and "revoked" — the platform does not distinguish the two once a
|
|
57
|
-
* grant is inactive; a consumer that needs to tell them apart should compare
|
|
58
|
-
* against its own last-known cached state.
|
|
59
|
-
*/
|
|
60
|
-
export type ConsentStatus = 'granted' | 'not_granted';
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* A standard, opaque reference to another plugin's record (RFC 0051). Store
|
|
64
|
-
* this shape (or split it into columns) in your own tables to point at a
|
|
65
|
-
* sibling plugin's data without a direct foreign key or copying private data.
|
|
66
|
-
* `resourceId` is opaque to the consumer — the provider decides its shape.
|
|
67
|
-
*
|
|
68
|
-
* References are never authorization: every dereference still goes through
|
|
69
|
-
* `sdk.data.query()` (or a tool contract) and the current user's live
|
|
70
|
-
* consent. Treat a stored reference as a nullable link — the provider may be
|
|
71
|
-
* uninstalled, disabled, have revoked consent, or have deleted the resource.
|
|
72
|
-
*/
|
|
73
|
-
export interface PluginReference {
|
|
74
|
-
/** The provider plugin's manifest `id`. */
|
|
75
|
-
providerId: string;
|
|
76
|
-
/** Provider-local resource kind (e.g. `"contact"`, `"invoice"`). */
|
|
77
|
-
resourceType: string;
|
|
78
|
-
/** Opaque to the consumer — a UUID, slug, or compound key the provider defines. */
|
|
79
|
-
resourceId: string;
|
|
80
|
-
/** The data contract this reference was resolved through, if any. */
|
|
81
|
-
contract?: string;
|
|
82
|
-
/** Contract major version at the time this reference was captured. */
|
|
83
|
-
version?: number;
|
|
84
|
-
/** A cached, possibly-stale display label — show it when the provider is unavailable. */
|
|
85
|
-
labelSnapshot?: string;
|
|
86
|
-
/** Consumer-defined extra metadata. */
|
|
87
|
-
metadata?: unknown;
|
|
88
|
-
/** ISO-8601 timestamp of when this reference was created. */
|
|
89
|
-
linkedAt: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function requestContext(): Promise<{
|
|
93
|
-
userId: string | null;
|
|
94
|
-
capabilities: readonly string[];
|
|
95
|
-
}> {
|
|
96
|
-
const h = await headers();
|
|
97
|
-
return {
|
|
98
|
-
userId: h.get('x-sovereign-user-id'),
|
|
99
|
-
capabilities: parseCapabilities(h.get('x-sovereign-user-capabilities')),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Plugin dependency discovery and cross-plugin references (RFC 0051).
|
|
105
|
-
*
|
|
106
|
-
* Lets a plugin check whether an optional sibling plugin is installed,
|
|
107
|
-
* enabled, and available to the current user before offering an integration,
|
|
108
|
-
* and defines the standard `PluginReference` shape for storing opaque links
|
|
109
|
-
* to another plugin's records. Complements `sdk.data` (RFC 0002, which reads
|
|
110
|
-
* the data) and RFC 0047 plugin tools (which act on it).
|
|
111
|
-
*
|
|
112
|
-
* ```ts
|
|
113
|
-
* const crm = await sdk.plugins.get('io.openfs.sovereign.crm');
|
|
114
|
-
* if (crm?.availableToUser) {
|
|
115
|
-
* // offer a "Link to contact" action
|
|
116
|
-
* }
|
|
117
|
-
*
|
|
118
|
-
* const status = await sdk.plugins.getConsentStatus({
|
|
119
|
-
* providerId: 'io.openfs.sovereign.crm',
|
|
120
|
-
* contract: 'crm.contacts',
|
|
121
|
-
* version: 1,
|
|
122
|
-
* });
|
|
123
|
-
* ```
|
|
124
|
-
*/
|
|
125
|
-
export const plugins = {
|
|
126
|
-
/** Discover one installed plugin's status. `null` when not installed. */
|
|
127
|
-
async get(id: string): Promise<PluginAvailability | null> {
|
|
128
|
-
const { userId, capabilities } = await requestContext();
|
|
129
|
-
return requireHost().plugins.get(id, userId, capabilities);
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
/** Discover installed plugins, optionally filtered to those providing a contract. */
|
|
133
|
-
async list(filter?: PluginListFilter): Promise<PluginAvailability[]> {
|
|
134
|
-
const { userId, capabilities } = await requestContext();
|
|
135
|
-
return requireHost().plugins.list(filter, userId, capabilities);
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
/** Whether the current user has granted this plugin's requested data contract. */
|
|
139
|
-
async getConsentStatus(ref: DataContractRef): Promise<ConsentStatus> {
|
|
140
|
-
const h = await headers();
|
|
141
|
-
const consumerId = h.get('x-sovereign-plugin-id');
|
|
142
|
-
const userId = h.get('x-sovereign-user-id');
|
|
143
|
-
if (!consumerId || !userId) return 'not_granted';
|
|
144
|
-
return requireHost().plugins.getConsentStatus(ref, consumerId, userId);
|
|
145
|
-
},
|
|
146
|
-
};
|
package/src/portability.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { headers } from 'next/headers';
|
|
2
|
-
import { requireHost } from './host';
|
|
3
|
-
import type { PluginReference } from './plugins';
|
|
4
|
-
|
|
5
|
-
/** Supplied by the runtime to an export resolver — scoped to the current user. */
|
|
6
|
-
export interface ExportContext {
|
|
7
|
-
userId: string;
|
|
8
|
-
tenantId: string;
|
|
9
|
-
/** User-selected export scope (RFC 0052). */
|
|
10
|
-
options: ExportOptions;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** User-selectable export scope, gathered by the Account Data tab. */
|
|
14
|
-
export interface ExportOptions {
|
|
15
|
-
/** Whether to include plugin-contributed file blobs. Defaults to `true`. */
|
|
16
|
-
includeFiles: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Supplied by the runtime to an import handler — scoped to the importing user. */
|
|
20
|
-
export interface ImportContext {
|
|
21
|
-
userId: string;
|
|
22
|
-
tenantId: string;
|
|
23
|
-
/**
|
|
24
|
-
* Maps an id from the source bundle to the id minted on this instance.
|
|
25
|
-
* Stable within one import: the same `originalId` always yields the same new
|
|
26
|
-
* id, so a plugin can preserve internal references (FKs) without breakage.
|
|
27
|
-
*/
|
|
28
|
-
remapId(originalId: string): string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Metadata about one of the plugin's own secrets, without its plaintext value. */
|
|
32
|
-
export interface ExportSecretMetadata {
|
|
33
|
-
/** The secret's user-facing label (e.g. "Personal API key"). */
|
|
34
|
-
label: string;
|
|
35
|
-
/** The secret's provider/purpose (e.g. "openai", "smtp"). */
|
|
36
|
-
provider: string;
|
|
37
|
-
/** Whether a value is currently stored — never the value itself. */
|
|
38
|
-
exists: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** One plugin's slice of a user's export. */
|
|
42
|
-
export interface PluginExportSection {
|
|
43
|
-
/** The contributing plugin's manifest `id`. */
|
|
44
|
-
pluginId: string;
|
|
45
|
-
/**
|
|
46
|
-
* The installed manifest `version` of the contributing plugin, for provenance.
|
|
47
|
-
* A resolver may omit this — the runtime always overwrites it with the
|
|
48
|
-
* installed manifest's actual `version` before writing the bundle, so a
|
|
49
|
-
* plugin cannot misreport its own version.
|
|
50
|
-
*/
|
|
51
|
-
pluginVersion?: string;
|
|
52
|
-
/** The plugin's own data-format version (for forward/backward compatibility). */
|
|
53
|
-
schemaVersion: number;
|
|
54
|
-
/** Plugin-defined JSON payload (rows, references). */
|
|
55
|
-
data: unknown;
|
|
56
|
-
/** Optional binary attachments, keyed by relative path within the section. */
|
|
57
|
-
blobs?: Record<string, Uint8Array>;
|
|
58
|
-
/**
|
|
59
|
-
* Metadata for secrets this plugin owns, scoped to the current user.
|
|
60
|
-
* Plaintext secret values must never appear here or anywhere in the export.
|
|
61
|
-
*/
|
|
62
|
-
secretMetadata?: ExportSecretMetadata[];
|
|
63
|
-
/** Non-fatal notices about this section (e.g. a file that was skipped). */
|
|
64
|
-
warnings?: string[];
|
|
65
|
-
/**
|
|
66
|
-
* Opaque links this plugin holds to other plugins' records (RFC 0051).
|
|
67
|
-
* Exported and imported as **inert metadata only** — the platform never
|
|
68
|
-
* dereferences them, whether or not the referenced provider plugin is also
|
|
69
|
-
* part of the same export. Importing a reference never grants access to
|
|
70
|
-
* the provider plugin; a live dereference still goes through
|
|
71
|
-
* `sdk.data.query()` and the current user's consent.
|
|
72
|
-
*/
|
|
73
|
-
references?: PluginReference[];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Produces one plugin's export section for the current user. */
|
|
77
|
-
export type ExportResolver = (ctx: ExportContext) => Promise<PluginExportSection>;
|
|
78
|
-
|
|
79
|
-
/** Restores one plugin's export section into the importing user's account. */
|
|
80
|
-
export type ImportHandler = (section: PluginExportSection, ctx: ImportContext) => Promise<void>;
|
|
81
|
-
|
|
82
|
-
/** Supplied to a deletion handler — scoped to the user being deleted. */
|
|
83
|
-
export interface DeletionContext {
|
|
84
|
-
userId: string;
|
|
85
|
-
tenantId: string;
|
|
86
|
-
/** The plugin's own Drizzle client (same as `sdk.db.getClient()`). */
|
|
87
|
-
db: unknown;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** What a deletion handler must return. */
|
|
91
|
-
export interface DeletionResult {
|
|
92
|
-
/** Number of rows (or objects) deleted. */
|
|
93
|
-
deleted: number;
|
|
94
|
-
/** Non-fatal errors encountered during cleanup. */
|
|
95
|
-
errors?: string[];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Cleans up one plugin's user data when a user account is deleted. */
|
|
99
|
-
export type DeletionHandler = (ctx: DeletionContext) => Promise<DeletionResult>;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* User data portability (RFC 0007, RFC 0052) — self-service export / restore /
|
|
103
|
-
* migration.
|
|
104
|
-
*
|
|
105
|
-
* A plugin opts in by registering an **export resolver** and an **import
|
|
106
|
-
* handler** (declare `data:export` / `data:import` in the manifest `permissions`).
|
|
107
|
-
* Both are runtime-mediated: the runtime supplies `ctx.userId` / `ctx.tenantId`,
|
|
108
|
-
* so a plugin only ever reads or writes the **current user's own slice** — never
|
|
109
|
-
* another user's, another plugin's, or another tenant's data.
|
|
110
|
-
*
|
|
111
|
-
* ```ts
|
|
112
|
-
* // register from your plugin's server code (a route handler or server
|
|
113
|
-
* // component that runs when the plugin is loaded):
|
|
114
|
-
* await sdk.portability.provideExport(async (ctx) => ({
|
|
115
|
-
* pluginId: 'com.example.tasks',
|
|
116
|
-
* schemaVersion: 1,
|
|
117
|
-
* data: await collectMyRowsFor(ctx.userId),
|
|
118
|
-
* // only when ctx.options.includeFiles is true — large attachments should
|
|
119
|
-
* // respect the user's export scope choice:
|
|
120
|
-
* blobs: ctx.options.includeFiles ? await collectMyBlobsFor(ctx.userId) : undefined,
|
|
121
|
-
* secretMetadata: await listMySecretMetadataFor(ctx.userId), // never plaintext
|
|
122
|
-
* }));
|
|
123
|
-
*
|
|
124
|
-
* await sdk.portability.provideImport(async (section, ctx) => {
|
|
125
|
-
* await restoreMyRowsFor(section.data, ctx); // use ctx.remapId for references
|
|
126
|
-
* });
|
|
127
|
-
* ```
|
|
128
|
-
*
|
|
129
|
-
* The platform exports/imports its own data (profile, preferences, avatar)
|
|
130
|
-
* directly; plugins contribute the rest. The user drives it from the
|
|
131
|
-
* Account → Data tab, including whether to include files/attachments
|
|
132
|
-
* (`ctx.options.includeFiles`). A resolver that throws is excluded from the
|
|
133
|
-
* bundle and recorded in the manifest's `failures` list — one plugin's failure
|
|
134
|
-
* never aborts the whole export.
|
|
135
|
-
*
|
|
136
|
-
* Registration reads the calling plugin's id from the request context, so these
|
|
137
|
-
* must run inside a plugin route (where `x-sovereign-plugin-id` is injected) —
|
|
138
|
-
* hence they are async. Registrations are in-process and reset on restart.
|
|
139
|
-
*/
|
|
140
|
-
export const portability = {
|
|
141
|
-
/** Provider: register this plugin's export resolver. */
|
|
142
|
-
async provideExport(resolver: ExportResolver): Promise<void> {
|
|
143
|
-
const pluginId = (await headers()).get('x-sovereign-plugin-id');
|
|
144
|
-
if (!pluginId) {
|
|
145
|
-
throw new Error(
|
|
146
|
-
'sdk.portability.provideExport() must be called from a plugin route context ' +
|
|
147
|
-
'(x-sovereign-plugin-id header missing).',
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
requireHost().portability.provideExport(pluginId, resolver);
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
/** Provider: register this plugin's import handler. */
|
|
154
|
-
async provideImport(handler: ImportHandler): Promise<void> {
|
|
155
|
-
const pluginId = (await headers()).get('x-sovereign-plugin-id');
|
|
156
|
-
if (!pluginId) {
|
|
157
|
-
throw new Error(
|
|
158
|
-
'sdk.portability.provideImport() must be called from a plugin route context ' +
|
|
159
|
-
'(x-sovereign-plugin-id header missing).',
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
requireHost().portability.provideImport(pluginId, handler);
|
|
163
|
-
},
|
|
164
|
-
|
|
165
|
-
/** Provider: register this plugin's deletion handler (RFC 0033). */
|
|
166
|
-
async provideDelete(handler: DeletionHandler): Promise<void> {
|
|
167
|
-
const pluginId = (await headers()).get('x-sovereign-plugin-id');
|
|
168
|
-
if (!pluginId) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
'sdk.portability.provideDelete() must be called from a plugin route context ' +
|
|
171
|
-
'(x-sovereign-plugin-id header missing).',
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
requireHost().portability.provideDelete(pluginId, handler);
|
|
175
|
-
},
|
|
176
|
-
};
|
package/src/secrets.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { headers } from 'next/headers';
|
|
2
|
-
import { NotAuthenticatedError } from './errors';
|
|
3
|
-
import { requireHost } from './host';
|
|
4
|
-
import type { CreateSecretInput, SecretContext, SecretRef, SecretScope } from './types';
|
|
5
|
-
|
|
6
|
-
const DEFAULT_TENANT_ID = 'default';
|
|
7
|
-
|
|
8
|
-
function parseCapabilities(raw: string | null): readonly string[] {
|
|
9
|
-
if (!raw) return [];
|
|
10
|
-
try {
|
|
11
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
12
|
-
return Array.isArray(parsed)
|
|
13
|
-
? parsed.filter((cap): cap is string => typeof cap === 'string')
|
|
14
|
-
: [];
|
|
15
|
-
} catch {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function secretContext(): Promise<SecretContext> {
|
|
21
|
-
const h = await headers();
|
|
22
|
-
const pluginId = h.get('x-sovereign-plugin-id');
|
|
23
|
-
if (!pluginId) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
'sdk.secrets requires a plugin route context (x-sovereign-plugin-id header missing).',
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return {
|
|
29
|
-
tenantId: DEFAULT_TENANT_ID,
|
|
30
|
-
pluginId,
|
|
31
|
-
userId: h.get('x-sovereign-user-id'),
|
|
32
|
-
capabilities: parseCapabilities(h.get('x-sovereign-user-capabilities')),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function requireUserForUserScope(scope: SecretScope, userId: string | null): void {
|
|
37
|
-
if (scope === 'user' && !userId) throw new NotAuthenticatedError();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Experimental platform-managed plugin secret vault (RFC 0043). */
|
|
41
|
-
export const secrets = {
|
|
42
|
-
async create(input: CreateSecretInput): Promise<SecretRef> {
|
|
43
|
-
const context = await secretContext();
|
|
44
|
-
requireUserForUserScope(input.scope, context.userId);
|
|
45
|
-
return requireHost().secrets.create(input, context);
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
async get(id: string): Promise<string | null> {
|
|
49
|
-
const context = await secretContext();
|
|
50
|
-
return requireHost().secrets.get(id, context);
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
async list(scope?: SecretScope): Promise<SecretRef[]> {
|
|
54
|
-
const context = await secretContext();
|
|
55
|
-
if (scope) requireUserForUserScope(scope, context.userId);
|
|
56
|
-
return requireHost().secrets.list(scope, context);
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
async update(id: string, value: string): Promise<SecretRef> {
|
|
60
|
-
const context = await secretContext();
|
|
61
|
-
return requireHost().secrets.update(id, value, context);
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
async delete(id: string): Promise<void> {
|
|
65
|
-
const context = await secretContext();
|
|
66
|
-
return requireHost().secrets.delete(id, context);
|
|
67
|
-
},
|
|
68
|
-
};
|
package/src/sessions.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { ActiveSession } from './types';
|
|
2
|
-
|
|
3
|
-
/** The session fields better-auth's list-sessions returns that we surface. */
|
|
4
|
-
export interface RawSession {
|
|
5
|
-
token: string;
|
|
6
|
-
userAgent?: string | null;
|
|
7
|
-
ipAddress?: string | null;
|
|
8
|
-
createdAt: string;
|
|
9
|
-
updatedAt: string;
|
|
10
|
-
expiresAt: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Project better-auth sessions into the SDK's `ActiveSession` shape, flag the
|
|
15
|
-
* one matching the current request's token, and order it first, then the rest
|
|
16
|
-
* newest-first (SRS ACC-05). Pure — the network fetch lives in `auth.ts`.
|
|
17
|
-
*/
|
|
18
|
-
export function markCurrentSessions(
|
|
19
|
-
sessions: readonly RawSession[],
|
|
20
|
-
currentToken: string | null,
|
|
21
|
-
): ActiveSession[] {
|
|
22
|
-
return sessions
|
|
23
|
-
.map((s) => ({
|
|
24
|
-
token: s.token,
|
|
25
|
-
current: s.token === currentToken,
|
|
26
|
-
userAgent: s.userAgent ?? null,
|
|
27
|
-
ipAddress: s.ipAddress ?? null,
|
|
28
|
-
createdAt: s.createdAt,
|
|
29
|
-
updatedAt: s.updatedAt,
|
|
30
|
-
expiresAt: s.expiresAt,
|
|
31
|
-
}))
|
|
32
|
-
.sort((a, b) => {
|
|
33
|
-
if (a.current !== b.current) return a.current ? -1 : 1;
|
|
34
|
-
return a.createdAt < b.createdAt ? 1 : -1;
|
|
35
|
-
});
|
|
36
|
-
}
|