@sovaria/sdk 0.0.0-stage → 0.4.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 +21 -0
- package/README.md +179 -2
- package/dist/client.d.ts +146 -0
- package/dist/client.js +583 -0
- package/dist/collections.d.ts +189 -0
- package/dist/collections.js +467 -0
- package/dist/complete-api.d.ts +47 -0
- package/dist/complete-api.js +64 -0
- package/dist/crashes.d.ts +79 -0
- package/dist/crashes.js +176 -0
- package/dist/crypto.d.ts +37 -0
- package/dist/crypto.js +189 -0
- package/dist/errors.d.ts +40 -0
- package/dist/errors.js +246 -0
- package/dist/firestore.d.ts +176 -0
- package/dist/firestore.js +337 -0
- package/dist/generated.d.ts +1227 -0
- package/dist/generated.js +1054 -0
- package/dist/index.d.ts +2046 -0
- package/dist/index.js +2287 -0
- package/dist/offline.d.ts +90 -0
- package/dist/offline.js +528 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.js +1 -0
- package/package.json +74 -4
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete API namespace factories for SovariaSDK parity with console endpoints.
|
|
3
|
+
*/
|
|
4
|
+
import type { JsonObject, JsonValue, RequestOptions } from './index.js';
|
|
5
|
+
type RequestFn = <T = JsonValue>(method: string, path: string, options?: RequestOptions) => Promise<T>;
|
|
6
|
+
export declare function createKeysApi(request: RequestFn): {
|
|
7
|
+
list: () => Promise<JsonValue>;
|
|
8
|
+
create: (payload: JsonObject) => Promise<JsonObject>;
|
|
9
|
+
ensureDefaults: () => Promise<JsonObject>;
|
|
10
|
+
revoke: (keyId: string) => Promise<JsonObject>;
|
|
11
|
+
};
|
|
12
|
+
export declare function createGovernanceApi(request: RequestFn): {
|
|
13
|
+
listRegions: () => Promise<JsonValue>;
|
|
14
|
+
listAvailableRegions: (query?: {
|
|
15
|
+
size?: string;
|
|
16
|
+
plan?: string;
|
|
17
|
+
}) => Promise<JsonValue>;
|
|
18
|
+
patchRegion: (regionId: string, body: JsonObject) => Promise<JsonObject>;
|
|
19
|
+
listTenants: () => Promise<JsonValue>;
|
|
20
|
+
getTenant: (orgId: string) => Promise<JsonObject>;
|
|
21
|
+
setTenantStatus: (orgId: string, body: JsonObject) => Promise<JsonObject>;
|
|
22
|
+
listTenantDatabases: (orgId: string) => Promise<JsonValue>;
|
|
23
|
+
provisionDatabase: (orgId: string, body: JsonObject) => Promise<JsonObject>;
|
|
24
|
+
getDatabaseConnection: (orgId: string, projectId: string) => Promise<JsonObject>;
|
|
25
|
+
/** Staff only: the connection string, with the active access grant that allows revealing it. */
|
|
26
|
+
revealDatabaseConnection: (orgId: string, projectId: string, grantId: string) => Promise<JsonObject>;
|
|
27
|
+
pauseDatabase: (orgId: string, projectId: string) => Promise<JsonObject>;
|
|
28
|
+
resumeDatabase: (orgId: string, projectId: string) => Promise<JsonObject>;
|
|
29
|
+
resizeDatabase: (orgId: string, projectId: string, body: JsonObject) => Promise<JsonObject>;
|
|
30
|
+
deleteDatabase: (orgId: string, projectId: string, snapshot?: boolean) => Promise<JsonObject>;
|
|
31
|
+
listDatabaseBackups: (orgId: string, projectId: string, limit?: number) => Promise<JsonValue>;
|
|
32
|
+
triggerDatabaseBackup: (orgId: string, projectId: string) => Promise<JsonObject>;
|
|
33
|
+
adminListDatabases: () => Promise<JsonValue>;
|
|
34
|
+
listHosts: (regionId?: string) => Promise<JsonValue>;
|
|
35
|
+
patchHost: (hostId: string, body: JsonObject) => Promise<JsonObject>;
|
|
36
|
+
metricsOverview: () => Promise<JsonObject>;
|
|
37
|
+
projectMetrics: (projectId: string, since?: string, limit?: number) => Promise<JsonObject>;
|
|
38
|
+
};
|
|
39
|
+
export declare function createAdminApi(request: RequestFn): {
|
|
40
|
+
listErrorTraces: (query?: Record<string, string | number | boolean | null | undefined>) => Promise<JsonValue>;
|
|
41
|
+
getErrorTrace: (traceId: string) => Promise<JsonObject>;
|
|
42
|
+
resolveErrorTrace: (traceId: string, body?: JsonObject) => Promise<JsonObject>;
|
|
43
|
+
usageOverview: (period?: string, limit?: number) => Promise<JsonObject>;
|
|
44
|
+
listAccessTransparency: (query: Record<string, string | number | boolean | null | undefined>) => Promise<JsonValue>;
|
|
45
|
+
gatewayHealth: () => Promise<JsonValue>;
|
|
46
|
+
};
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete API namespace factories for SovariaSDK parity with console endpoints.
|
|
3
|
+
*/
|
|
4
|
+
function safePathSegment(value) {
|
|
5
|
+
return encodeURIComponent(value);
|
|
6
|
+
}
|
|
7
|
+
export function createKeysApi(request) {
|
|
8
|
+
return {
|
|
9
|
+
list: () => request('GET', '/api/v1/apikeys', { requiresScope: true }),
|
|
10
|
+
create: (payload) => request('POST', '/api/v1/apikeys', { body: payload, requiresScope: true }),
|
|
11
|
+
ensureDefaults: () => request('POST', '/api/v1/apikeys/ensure-defaults', {
|
|
12
|
+
body: {},
|
|
13
|
+
requiresScope: true,
|
|
14
|
+
}),
|
|
15
|
+
revoke: (keyId) => request('DELETE', `/api/v1/apikeys/${safePathSegment(keyId)}`, {
|
|
16
|
+
requiresScope: true,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function createGovernanceApi(request) {
|
|
21
|
+
const gov = (suffix) => `/api/v1/governance/${suffix.replace(/^\//, '')}`;
|
|
22
|
+
return {
|
|
23
|
+
listRegions: () => request('GET', gov('regions')),
|
|
24
|
+
listAvailableRegions: (query) => request('GET', gov('regions/available'), { query }),
|
|
25
|
+
patchRegion: (regionId, body) => request('PATCH', gov(`regions/${safePathSegment(regionId)}`), { body }),
|
|
26
|
+
listTenants: () => request('GET', gov('tenants')),
|
|
27
|
+
getTenant: (orgId) => request('GET', gov(`tenants/${safePathSegment(orgId)}`)),
|
|
28
|
+
setTenantStatus: (orgId, body) => request('PATCH', gov(`tenants/${safePathSegment(orgId)}/status`), { body }),
|
|
29
|
+
listTenantDatabases: (orgId) => request('GET', gov(`tenants/${safePathSegment(orgId)}/databases`)),
|
|
30
|
+
provisionDatabase: (orgId, body) => request('POST', gov(`tenants/${safePathSegment(orgId)}/databases/provision`), {
|
|
31
|
+
body,
|
|
32
|
+
}),
|
|
33
|
+
getDatabaseConnection: (orgId, projectId) => request('GET', gov(`tenants/${safePathSegment(orgId)}/databases/${safePathSegment(projectId)}/connection`)),
|
|
34
|
+
/** Staff only: the connection string, with the active access grant that allows revealing it. */
|
|
35
|
+
revealDatabaseConnection: (orgId, projectId, grantId) => request('POST', gov(`tenants/${safePathSegment(orgId)}/databases/${safePathSegment(projectId)}/connection/reveal`), { body: { grant_id: grantId } }),
|
|
36
|
+
pauseDatabase: (orgId, projectId) => request('POST', gov(`tenants/${safePathSegment(orgId)}/databases/${safePathSegment(projectId)}/pause`), { body: {} }),
|
|
37
|
+
resumeDatabase: (orgId, projectId) => request('POST', gov(`tenants/${safePathSegment(orgId)}/databases/${safePathSegment(projectId)}/resume`), { body: {} }),
|
|
38
|
+
resizeDatabase: (orgId, projectId, body) => request('POST', gov(`tenants/${safePathSegment(orgId)}/databases/${safePathSegment(projectId)}/resize`), { body }),
|
|
39
|
+
deleteDatabase: (orgId, projectId, snapshot = true) => request('DELETE', gov(`tenants/${safePathSegment(orgId)}/databases/${safePathSegment(projectId)}`), { query: { snapshot } }),
|
|
40
|
+
listDatabaseBackups: (orgId, projectId, limit = 5) => request('GET', gov(`tenants/${safePathSegment(orgId)}/databases/${safePathSegment(projectId)}/backups`), { query: { limit } }),
|
|
41
|
+
triggerDatabaseBackup: (orgId, projectId) => request('POST', gov(`tenants/${safePathSegment(orgId)}/databases/${safePathSegment(projectId)}/backups`), { body: {} }),
|
|
42
|
+
adminListDatabases: () => request('GET', gov('admin/databases')),
|
|
43
|
+
listHosts: (regionId) => request('GET', gov('hosts'), { query: regionId ? { region_id: regionId } : undefined }),
|
|
44
|
+
patchHost: (hostId, body) => request('PATCH', gov(`hosts/${safePathSegment(hostId)}`), { body }),
|
|
45
|
+
metricsOverview: () => request('GET', gov('metrics/overview')),
|
|
46
|
+
projectMetrics: (projectId, since = '24', limit = 500) => request('GET', gov(`metrics/projects/${safePathSegment(projectId)}`), {
|
|
47
|
+
query: { since, limit },
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function createAdminApi(request) {
|
|
52
|
+
return {
|
|
53
|
+
listErrorTraces: (query) => request('GET', '/api/v1/error-traces', { query }),
|
|
54
|
+
getErrorTrace: (traceId) => request('GET', `/api/v1/error-traces/${safePathSegment(traceId)}`),
|
|
55
|
+
resolveErrorTrace: (traceId, body = {}) => request('PATCH', `/api/v1/error-traces/${safePathSegment(traceId)}/resolved`, {
|
|
56
|
+
body,
|
|
57
|
+
}),
|
|
58
|
+
usageOverview: (period, limit = 100) => request('GET', '/api/v1/admin/usage/overview', {
|
|
59
|
+
query: { period, limit },
|
|
60
|
+
}),
|
|
61
|
+
listAccessTransparency: (query) => request('GET', '/api/v1/access-transparency', { query }),
|
|
62
|
+
gatewayHealth: () => request('GET', '/healthz', { requiresAuth: false }),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App crash reporting (plan C5): uncaught errors from your app, grouped into issues in the console
|
|
3
|
+
* (Observability › Crashes).
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* sov.crashes.install({ release: '1.4.2' }) // browser + React Native: uncaught errors and rejections
|
|
7
|
+
* try { await checkout() } catch (e) { sov.crashes.capture(e, { tags: { step: 'pay' } }) }
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Each report carries the error type, message and stack, the release, the platform and device, the
|
|
11
|
+
* last 50 breadcrumbs (Sovaria requests the SDK made, plus your own `addBreadcrumb`) and the signed-in
|
|
12
|
+
* user (the server takes the user from the request's token, not from the report). Reports are batched,
|
|
13
|
+
* duplicates within a second are dropped and at most 30 a minute are sent, so a crash loop can't flood
|
|
14
|
+
* your plan's `obs.crash_events_month`.
|
|
15
|
+
*/
|
|
16
|
+
export interface CrashEvent {
|
|
17
|
+
type: string;
|
|
18
|
+
message: string;
|
|
19
|
+
stack: string[];
|
|
20
|
+
platform: string;
|
|
21
|
+
release?: string;
|
|
22
|
+
environment?: string;
|
|
23
|
+
handled: boolean;
|
|
24
|
+
url?: string;
|
|
25
|
+
device?: Record<string, unknown>;
|
|
26
|
+
breadcrumbs: Breadcrumb[];
|
|
27
|
+
tags?: Record<string, string>;
|
|
28
|
+
extra?: Record<string, unknown>;
|
|
29
|
+
fingerprint?: string;
|
|
30
|
+
timestamp: string;
|
|
31
|
+
}
|
|
32
|
+
export interface Breadcrumb {
|
|
33
|
+
category: string;
|
|
34
|
+
message: string;
|
|
35
|
+
data?: Record<string, unknown>;
|
|
36
|
+
timestamp: string;
|
|
37
|
+
}
|
|
38
|
+
export interface CrashOptions {
|
|
39
|
+
release?: string;
|
|
40
|
+
environment?: string;
|
|
41
|
+
/** 0..1: the share of crashes to send (default 1). */
|
|
42
|
+
sampleRate?: number;
|
|
43
|
+
/** Change or drop (return null) a report before it is sent. */
|
|
44
|
+
beforeSend?: (event: CrashEvent) => CrashEvent | null;
|
|
45
|
+
}
|
|
46
|
+
type Sender = (events: CrashEvent[]) => Promise<unknown>;
|
|
47
|
+
export declare class CrashReporter {
|
|
48
|
+
private readonly send;
|
|
49
|
+
private options;
|
|
50
|
+
private queue;
|
|
51
|
+
private crumbs;
|
|
52
|
+
private recent;
|
|
53
|
+
private sentThisMinute;
|
|
54
|
+
private minute;
|
|
55
|
+
private timer?;
|
|
56
|
+
private uninstallers;
|
|
57
|
+
installed: boolean;
|
|
58
|
+
constructor(send: Sender);
|
|
59
|
+
/** Report uncaught errors and unhandled promise rejections (browser and React Native). Call once. */
|
|
60
|
+
install(options?: CrashOptions): () => void;
|
|
61
|
+
uninstall(): void;
|
|
62
|
+
addBreadcrumb(crumb: {
|
|
63
|
+
category?: string;
|
|
64
|
+
message: string;
|
|
65
|
+
data?: Record<string, unknown>;
|
|
66
|
+
}): void;
|
|
67
|
+
/** Report an error you caught (handled) or one the app didn't. Returns false when it was dropped. */
|
|
68
|
+
capture(error: unknown, context?: {
|
|
69
|
+
handled?: boolean;
|
|
70
|
+
tags?: Record<string, string>;
|
|
71
|
+
extra?: Record<string, unknown>;
|
|
72
|
+
fingerprint?: string;
|
|
73
|
+
}): boolean;
|
|
74
|
+
private schedule;
|
|
75
|
+
/** Send queued reports now. */
|
|
76
|
+
flush(): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
export declare function parseStack(error: Error): string[];
|
|
79
|
+
export {};
|
package/dist/crashes.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App crash reporting (plan C5): uncaught errors from your app, grouped into issues in the console
|
|
3
|
+
* (Observability › Crashes).
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* sov.crashes.install({ release: '1.4.2' }) // browser + React Native: uncaught errors and rejections
|
|
7
|
+
* try { await checkout() } catch (e) { sov.crashes.capture(e, { tags: { step: 'pay' } }) }
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* Each report carries the error type, message and stack, the release, the platform and device, the
|
|
11
|
+
* last 50 breadcrumbs (Sovaria requests the SDK made, plus your own `addBreadcrumb`) and the signed-in
|
|
12
|
+
* user (the server takes the user from the request's token, not from the report). Reports are batched,
|
|
13
|
+
* duplicates within a second are dropped and at most 30 a minute are sent, so a crash loop can't flood
|
|
14
|
+
* your plan's `obs.crash_events_month`.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_BREADCRUMBS = 50;
|
|
17
|
+
const MAX_PER_MINUTE = 30;
|
|
18
|
+
export class CrashReporter {
|
|
19
|
+
send;
|
|
20
|
+
options = {};
|
|
21
|
+
queue = [];
|
|
22
|
+
crumbs = [];
|
|
23
|
+
recent = new Map();
|
|
24
|
+
sentThisMinute = 0;
|
|
25
|
+
minute = 0;
|
|
26
|
+
timer;
|
|
27
|
+
uninstallers = [];
|
|
28
|
+
installed = false;
|
|
29
|
+
constructor(send) {
|
|
30
|
+
this.send = send;
|
|
31
|
+
}
|
|
32
|
+
/** Report uncaught errors and unhandled promise rejections (browser and React Native). Call once. */
|
|
33
|
+
install(options = {}) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
if (this.installed)
|
|
36
|
+
return () => this.uninstall();
|
|
37
|
+
this.installed = true;
|
|
38
|
+
const g = globalThis;
|
|
39
|
+
if (typeof g.addEventListener === 'function') {
|
|
40
|
+
const onError = (e) => this.capture(e.error ?? new Error(String(e.message ?? 'Script error')), { handled: false });
|
|
41
|
+
const onRejection = (e) => this.capture(e.reason ?? new Error('Unhandled promise rejection'), { handled: false });
|
|
42
|
+
g.addEventListener('error', onError);
|
|
43
|
+
g.addEventListener('unhandledrejection', onRejection);
|
|
44
|
+
const onHide = () => void this.flush();
|
|
45
|
+
g.addEventListener('pagehide', onHide);
|
|
46
|
+
this.uninstallers.push(() => {
|
|
47
|
+
g.removeEventListener?.('error', onError);
|
|
48
|
+
g.removeEventListener?.('unhandledrejection', onRejection);
|
|
49
|
+
g.removeEventListener?.('pagehide', onHide);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// React Native: wrap the global handler, then call the previous one (keeps the red box / crash).
|
|
53
|
+
const errorUtils = g.ErrorUtils;
|
|
54
|
+
if (errorUtils?.setGlobalHandler && errorUtils.getGlobalHandler) {
|
|
55
|
+
const previous = errorUtils.getGlobalHandler();
|
|
56
|
+
errorUtils.setGlobalHandler((error, fatal) => {
|
|
57
|
+
this.capture(error, { handled: false, tags: { fatal: String(Boolean(fatal)) } });
|
|
58
|
+
void this.flush();
|
|
59
|
+
previous?.(error, fatal);
|
|
60
|
+
});
|
|
61
|
+
this.uninstallers.push(() => errorUtils.setGlobalHandler?.(previous));
|
|
62
|
+
}
|
|
63
|
+
return () => this.uninstall();
|
|
64
|
+
}
|
|
65
|
+
uninstall() {
|
|
66
|
+
for (const off of this.uninstallers.splice(0))
|
|
67
|
+
off();
|
|
68
|
+
this.installed = false;
|
|
69
|
+
}
|
|
70
|
+
addBreadcrumb(crumb) {
|
|
71
|
+
this.crumbs.push({ category: crumb.category ?? 'app', message: String(crumb.message).slice(0, 300), data: crumb.data,
|
|
72
|
+
timestamp: new Date().toISOString() });
|
|
73
|
+
if (this.crumbs.length > MAX_BREADCRUMBS)
|
|
74
|
+
this.crumbs.splice(0, this.crumbs.length - MAX_BREADCRUMBS);
|
|
75
|
+
}
|
|
76
|
+
/** Report an error you caught (handled) or one the app didn't. Returns false when it was dropped. */
|
|
77
|
+
capture(error, context = {}) {
|
|
78
|
+
const err = error instanceof Error ? error : new Error(typeof error === 'string' ? error : JSON.stringify(error ?? 'Unknown error'));
|
|
79
|
+
if ((this.options.sampleRate ?? 1) < 1 && Math.random() >= (this.options.sampleRate ?? 1))
|
|
80
|
+
return false;
|
|
81
|
+
const stack = parseStack(err);
|
|
82
|
+
const key = `${err.name}|${err.message}|${stack[0] ?? ''}`;
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if ((this.recent.get(key) ?? 0) > now - 1000)
|
|
85
|
+
return false;
|
|
86
|
+
this.recent.set(key, now);
|
|
87
|
+
if (this.recent.size > 200)
|
|
88
|
+
this.recent.clear();
|
|
89
|
+
const minute = Math.floor(now / 60_000);
|
|
90
|
+
if (minute !== this.minute) {
|
|
91
|
+
this.minute = minute;
|
|
92
|
+
this.sentThisMinute = 0;
|
|
93
|
+
}
|
|
94
|
+
if (this.sentThisMinute >= MAX_PER_MINUTE)
|
|
95
|
+
return false;
|
|
96
|
+
let event = {
|
|
97
|
+
type: err.name || 'Error',
|
|
98
|
+
message: err.message,
|
|
99
|
+
stack,
|
|
100
|
+
platform: detectPlatform(),
|
|
101
|
+
release: this.options.release,
|
|
102
|
+
environment: this.options.environment,
|
|
103
|
+
handled: context.handled ?? true,
|
|
104
|
+
url: currentUrl(),
|
|
105
|
+
device: deviceInfo(),
|
|
106
|
+
breadcrumbs: [...this.crumbs],
|
|
107
|
+
tags: context.tags,
|
|
108
|
+
extra: context.extra,
|
|
109
|
+
fingerprint: context.fingerprint,
|
|
110
|
+
timestamp: new Date().toISOString(),
|
|
111
|
+
};
|
|
112
|
+
if (this.options.beforeSend)
|
|
113
|
+
event = this.options.beforeSend(event);
|
|
114
|
+
if (!event)
|
|
115
|
+
return false;
|
|
116
|
+
this.sentThisMinute++;
|
|
117
|
+
this.queue.push(event);
|
|
118
|
+
this.schedule();
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
schedule() {
|
|
122
|
+
if (this.timer)
|
|
123
|
+
return;
|
|
124
|
+
this.timer = setTimeout(() => void this.flush(), 1000);
|
|
125
|
+
}
|
|
126
|
+
/** Send queued reports now. */
|
|
127
|
+
async flush() {
|
|
128
|
+
if (this.timer) {
|
|
129
|
+
clearTimeout(this.timer);
|
|
130
|
+
this.timer = undefined;
|
|
131
|
+
}
|
|
132
|
+
while (this.queue.length) {
|
|
133
|
+
const batch = this.queue.splice(0, 20);
|
|
134
|
+
try {
|
|
135
|
+
await this.send(batch);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return; // never let crash reporting itself crash the app; the batch is dropped
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function parseStack(error) {
|
|
144
|
+
return String(error.stack ?? '')
|
|
145
|
+
.split('\n')
|
|
146
|
+
.map((l) => l.trim())
|
|
147
|
+
.filter((l) => l && !l.startsWith(`${error.name}:`) && l !== error.name)
|
|
148
|
+
.slice(0, 50);
|
|
149
|
+
}
|
|
150
|
+
function detectPlatform() {
|
|
151
|
+
const g = globalThis;
|
|
152
|
+
if (g.navigator?.product === 'ReactNative')
|
|
153
|
+
return 'react-native';
|
|
154
|
+
if (typeof g.window !== 'undefined' && typeof g.document !== 'undefined')
|
|
155
|
+
return 'web';
|
|
156
|
+
if (g.process?.versions?.node)
|
|
157
|
+
return 'node';
|
|
158
|
+
return 'javascript';
|
|
159
|
+
}
|
|
160
|
+
function currentUrl() {
|
|
161
|
+
const g = globalThis;
|
|
162
|
+
return typeof g.location?.href === 'string' ? g.location.href : undefined;
|
|
163
|
+
}
|
|
164
|
+
function deviceInfo() {
|
|
165
|
+
const g = globalThis;
|
|
166
|
+
const out = {};
|
|
167
|
+
if (g.navigator?.userAgent)
|
|
168
|
+
out.user_agent = String(g.navigator.userAgent).slice(0, 300);
|
|
169
|
+
if (g.navigator?.language)
|
|
170
|
+
out.language = g.navigator.language;
|
|
171
|
+
if (g.screen?.width)
|
|
172
|
+
out.screen = `${g.screen.width}x${g.screen.height}`;
|
|
173
|
+
if (g.process?.versions?.node)
|
|
174
|
+
out.node = g.process.versions.node;
|
|
175
|
+
return out;
|
|
176
|
+
}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Argon2id parameters (OWASP recommended minimums, matching Python SDK). */
|
|
2
|
+
export declare const ARGON2_TIME_COST = 3;
|
|
3
|
+
export declare const ARGON2_MEMORY_COST = 65536;
|
|
4
|
+
export declare const ARGON2_PARALLELISM = 1;
|
|
5
|
+
export declare const ARGON2_HASH_LEN = 32;
|
|
6
|
+
export declare const ARGON2_SALT_LEN = 16;
|
|
7
|
+
export declare const DEK_SIZE_BYTES = 32;
|
|
8
|
+
export declare const NONCE_SIZE_BYTES = 12;
|
|
9
|
+
export interface EncryptedEnvelope {
|
|
10
|
+
ciphertext: string;
|
|
11
|
+
nonce: string;
|
|
12
|
+
auth_tag: string;
|
|
13
|
+
encrypted_dek: string;
|
|
14
|
+
dek_nonce: string;
|
|
15
|
+
payload_hash: string;
|
|
16
|
+
salt?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Derive a 256-bit KEK from password + salt using Argon2id. */
|
|
19
|
+
export declare function deriveKek(password: string, salt: Uint8Array): Promise<Uint8Array>;
|
|
20
|
+
/** Generate a 128-bit cryptographically random salt. */
|
|
21
|
+
export declare function generateSalt(): Uint8Array;
|
|
22
|
+
/** Parse a hex-encoded salt string into bytes. */
|
|
23
|
+
export declare function hexToBytes(hex: string): Uint8Array;
|
|
24
|
+
/** Encode bytes as a lowercase hex string. */
|
|
25
|
+
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
26
|
+
/** Best-effort RAM wipe for sensitive byte buffers. */
|
|
27
|
+
export declare function zeroBytes(data: Uint8Array | ArrayBuffer): void;
|
|
28
|
+
/** SHA-256(ciphertext_b64 || nonce_b64 || auth_tag_b64) for Gateway L11 verification. */
|
|
29
|
+
export declare function computePayloadHash(envelope: Pick<EncryptedEnvelope, 'ciphertext' | 'nonce' | 'auth_tag'>): Promise<string>;
|
|
30
|
+
/** Full Zero-Knowledge envelope encryption (derives KEK from password). */
|
|
31
|
+
export declare function encryptDocument(plaintext: Uint8Array, password: string, salt: Uint8Array): Promise<EncryptedEnvelope>;
|
|
32
|
+
/** Encrypt using a pre-derived KEK (skips Argon2id). */
|
|
33
|
+
export declare function encryptDocumentWithKek(plaintext: Uint8Array, kek: Uint8Array): Promise<EncryptedEnvelope>;
|
|
34
|
+
/** Reverse envelope encryption using password-derived KEK. */
|
|
35
|
+
export declare function decryptDocument(envelope: EncryptedEnvelope, password: string, salt: Uint8Array): Promise<Uint8Array>;
|
|
36
|
+
/** Decrypt using a pre-derived KEK (skips Argon2id). */
|
|
37
|
+
export declare function decryptDocumentWithKek(envelope: EncryptedEnvelope, kek: Uint8Array): Promise<Uint8Array>;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { argon2id } from 'hash-wasm';
|
|
2
|
+
/** Argon2id parameters (OWASP recommended minimums, matching Python SDK). */
|
|
3
|
+
export const ARGON2_TIME_COST = 3;
|
|
4
|
+
export const ARGON2_MEMORY_COST = 65536; // 64 MB in KiB
|
|
5
|
+
export const ARGON2_PARALLELISM = 1;
|
|
6
|
+
export const ARGON2_HASH_LEN = 32;
|
|
7
|
+
export const ARGON2_SALT_LEN = 16;
|
|
8
|
+
export const DEK_SIZE_BYTES = 32;
|
|
9
|
+
export const NONCE_SIZE_BYTES = 12;
|
|
10
|
+
function getSubtleCrypto() {
|
|
11
|
+
const subtle = globalThis.crypto?.subtle;
|
|
12
|
+
if (!subtle) {
|
|
13
|
+
throw new Error('Web Crypto API (crypto.subtle) is not available in this environment');
|
|
14
|
+
}
|
|
15
|
+
return subtle;
|
|
16
|
+
}
|
|
17
|
+
function randomBytes(length) {
|
|
18
|
+
const bytes = new Uint8Array(length);
|
|
19
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
function bytesToBase64(bytes) {
|
|
23
|
+
let binary = '';
|
|
24
|
+
const chunk = 0x8000;
|
|
25
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
26
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
27
|
+
}
|
|
28
|
+
return btoa(binary);
|
|
29
|
+
}
|
|
30
|
+
function base64ToBytes(value) {
|
|
31
|
+
const binary = atob(value);
|
|
32
|
+
const bytes = new Uint8Array(binary.length);
|
|
33
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
34
|
+
bytes[i] = binary.charCodeAt(i);
|
|
35
|
+
}
|
|
36
|
+
return bytes;
|
|
37
|
+
}
|
|
38
|
+
function concatBytes(...parts) {
|
|
39
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
40
|
+
const out = new Uint8Array(total);
|
|
41
|
+
let offset = 0;
|
|
42
|
+
for (const part of parts) {
|
|
43
|
+
out.set(part, offset);
|
|
44
|
+
offset += part.length;
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function toBufferSource(data) {
|
|
49
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
50
|
+
}
|
|
51
|
+
async function sha256Hex(data) {
|
|
52
|
+
const digest = await getSubtleCrypto().digest('SHA-256', toBufferSource(data));
|
|
53
|
+
return Array.from(new Uint8Array(digest))
|
|
54
|
+
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
55
|
+
.join('');
|
|
56
|
+
}
|
|
57
|
+
async function importAesKey(keyBytes, usages) {
|
|
58
|
+
return getSubtleCrypto().importKey('raw', toBufferSource(keyBytes), { name: 'AES-GCM' }, false, usages);
|
|
59
|
+
}
|
|
60
|
+
async function aesGcmEncrypt(keyBytes, nonce, plaintext) {
|
|
61
|
+
const key = await importAesKey(keyBytes, ['encrypt']);
|
|
62
|
+
const full = new Uint8Array(await getSubtleCrypto().encrypt({ name: 'AES-GCM', iv: toBufferSource(nonce), tagLength: 128 }, key, toBufferSource(plaintext)));
|
|
63
|
+
return {
|
|
64
|
+
ciphertext: full.slice(0, full.length - 16),
|
|
65
|
+
authTag: full.slice(full.length - 16),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function aesGcmDecrypt(keyBytes, nonce, ciphertext, authTag) {
|
|
69
|
+
const key = await importAesKey(keyBytes, ['decrypt']);
|
|
70
|
+
const full = concatBytes(ciphertext, authTag);
|
|
71
|
+
return new Uint8Array(await getSubtleCrypto().decrypt({ name: 'AES-GCM', iv: toBufferSource(nonce), tagLength: 128 }, key, toBufferSource(full)));
|
|
72
|
+
}
|
|
73
|
+
async function aesGcmDecryptCombined(keyBytes, nonce, ciphertextWithTag) {
|
|
74
|
+
const key = await importAesKey(keyBytes, ['decrypt']);
|
|
75
|
+
return new Uint8Array(await getSubtleCrypto().decrypt({ name: 'AES-GCM', iv: toBufferSource(nonce), tagLength: 128 }, key, toBufferSource(ciphertextWithTag)));
|
|
76
|
+
}
|
|
77
|
+
async function buildEnvelopeFromParts(docCiphertext, docNonce, authTag, encryptedDek, dekNonce) {
|
|
78
|
+
const ciphertext = bytesToBase64(docCiphertext);
|
|
79
|
+
const nonce = bytesToBase64(docNonce);
|
|
80
|
+
const auth_tag = bytesToBase64(authTag);
|
|
81
|
+
const payload_hash = await computePayloadHash({ ciphertext, nonce, auth_tag });
|
|
82
|
+
return {
|
|
83
|
+
ciphertext,
|
|
84
|
+
nonce,
|
|
85
|
+
auth_tag,
|
|
86
|
+
encrypted_dek: bytesToBase64(encryptedDek),
|
|
87
|
+
dek_nonce: bytesToBase64(dekNonce),
|
|
88
|
+
payload_hash,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Derive a 256-bit KEK from password + salt using Argon2id. */
|
|
92
|
+
export async function deriveKek(password, salt) {
|
|
93
|
+
const hash = await argon2id({
|
|
94
|
+
password,
|
|
95
|
+
salt,
|
|
96
|
+
iterations: ARGON2_TIME_COST,
|
|
97
|
+
memorySize: ARGON2_MEMORY_COST,
|
|
98
|
+
parallelism: ARGON2_PARALLELISM,
|
|
99
|
+
hashLength: ARGON2_HASH_LEN,
|
|
100
|
+
outputType: 'binary',
|
|
101
|
+
});
|
|
102
|
+
return hash instanceof Uint8Array ? hash : new Uint8Array(hash);
|
|
103
|
+
}
|
|
104
|
+
/** Generate a 128-bit cryptographically random salt. */
|
|
105
|
+
export function generateSalt() {
|
|
106
|
+
return randomBytes(ARGON2_SALT_LEN);
|
|
107
|
+
}
|
|
108
|
+
/** Parse a hex-encoded salt string into bytes. */
|
|
109
|
+
export function hexToBytes(hex) {
|
|
110
|
+
const normalized = hex.trim();
|
|
111
|
+
if (normalized.length % 2 !== 0) {
|
|
112
|
+
throw new Error('Invalid hex string length');
|
|
113
|
+
}
|
|
114
|
+
const bytes = new Uint8Array(normalized.length / 2);
|
|
115
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
116
|
+
bytes[i] = Number.parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
|
|
117
|
+
}
|
|
118
|
+
return bytes;
|
|
119
|
+
}
|
|
120
|
+
/** Encode bytes as a lowercase hex string. */
|
|
121
|
+
export function bytesToHex(bytes) {
|
|
122
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
123
|
+
}
|
|
124
|
+
/** Best-effort RAM wipe for sensitive byte buffers. */
|
|
125
|
+
export function zeroBytes(data) {
|
|
126
|
+
const view = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
127
|
+
for (let i = 0; i < view.length; i += 1) {
|
|
128
|
+
view[i] = 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** SHA-256(ciphertext_b64 || nonce_b64 || auth_tag_b64) for Gateway L11 verification. */
|
|
132
|
+
export async function computePayloadHash(envelope) {
|
|
133
|
+
const payload = new TextEncoder().encode(envelope.ciphertext + envelope.nonce + envelope.auth_tag);
|
|
134
|
+
return sha256Hex(payload);
|
|
135
|
+
}
|
|
136
|
+
/** Full Zero-Knowledge envelope encryption (derives KEK from password). */
|
|
137
|
+
export async function encryptDocument(plaintext, password, salt) {
|
|
138
|
+
const kek = await deriveKek(password, salt);
|
|
139
|
+
const dek = randomBytes(DEK_SIZE_BYTES);
|
|
140
|
+
try {
|
|
141
|
+
const docNonce = randomBytes(NONCE_SIZE_BYTES);
|
|
142
|
+
const { ciphertext: docCiphertext, authTag } = await aesGcmEncrypt(dek, docNonce, plaintext);
|
|
143
|
+
const dekNonce = randomBytes(NONCE_SIZE_BYTES);
|
|
144
|
+
const encryptedDek = await aesGcmEncryptCombined(kek, dekNonce, dek);
|
|
145
|
+
return await buildEnvelopeFromParts(docCiphertext, docNonce, authTag, encryptedDek, dekNonce);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
zeroBytes(dek);
|
|
149
|
+
zeroBytes(kek);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function aesGcmEncryptCombined(keyBytes, nonce, plaintext) {
|
|
153
|
+
const key = await importAesKey(keyBytes, ['encrypt']);
|
|
154
|
+
return new Uint8Array(await getSubtleCrypto().encrypt({ name: 'AES-GCM', iv: toBufferSource(nonce), tagLength: 128 }, key, toBufferSource(plaintext)));
|
|
155
|
+
}
|
|
156
|
+
/** Encrypt using a pre-derived KEK (skips Argon2id). */
|
|
157
|
+
export async function encryptDocumentWithKek(plaintext, kek) {
|
|
158
|
+
const dek = randomBytes(DEK_SIZE_BYTES);
|
|
159
|
+
try {
|
|
160
|
+
const docNonce = randomBytes(NONCE_SIZE_BYTES);
|
|
161
|
+
const { ciphertext: docCiphertext, authTag } = await aesGcmEncrypt(dek, docNonce, plaintext);
|
|
162
|
+
const dekNonce = randomBytes(NONCE_SIZE_BYTES);
|
|
163
|
+
const encryptedDek = await aesGcmEncryptCombined(kek, dekNonce, dek);
|
|
164
|
+
return await buildEnvelopeFromParts(docCiphertext, docNonce, authTag, encryptedDek, dekNonce);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
zeroBytes(dek);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Reverse envelope encryption using password-derived KEK. */
|
|
171
|
+
export async function decryptDocument(envelope, password, salt) {
|
|
172
|
+
const kek = await deriveKek(password, salt);
|
|
173
|
+
try {
|
|
174
|
+
return await decryptDocumentWithKek(envelope, kek);
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
zeroBytes(kek);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Decrypt using a pre-derived KEK (skips Argon2id). */
|
|
181
|
+
export async function decryptDocumentWithKek(envelope, kek) {
|
|
182
|
+
const dek = await aesGcmDecryptCombined(kek, base64ToBytes(envelope.dek_nonce), base64ToBytes(envelope.encrypted_dek));
|
|
183
|
+
try {
|
|
184
|
+
return aesGcmDecrypt(dek, base64ToBytes(envelope.nonce), base64ToBytes(envelope.ciphertext), base64ToBytes(envelope.auth_tag));
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
zeroBytes(dek);
|
|
188
|
+
}
|
|
189
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors thrown by the SDK.
|
|
3
|
+
*
|
|
4
|
+
* `message` is written for the people using your app ("You don't have permission to do that."),
|
|
5
|
+
* so `catch (e) { show(e.message) }` never leaks policy names, table names or SQL to end users.
|
|
6
|
+
* The server's own text stays available for developers in `technicalMessage` / `detail`,
|
|
7
|
+
* and `code` is a stable value to branch on.
|
|
8
|
+
*/
|
|
9
|
+
export type SovariaErrorCode = 'NETWORK_ERROR' | 'UNAUTHENTICATED' | 'PERMISSION_DENIED' | 'NOT_FOUND' | 'CONFLICT' | 'INVALID_INPUT' | 'PAYLOAD_TOO_LARGE' | 'UNSUPPORTED_TYPE' | 'RATE_LIMITED' | 'PLAN_LIMIT_REACHED' | 'PLAN_FEATURE_REQUIRED' | 'PLAN_QUOTA_EXHAUSTED' | 'SERVICE_UNAVAILABLE' | 'SERVER_ERROR' | 'BAD_REQUEST' | 'CONFIG_ERROR' | 'UNKNOWN';
|
|
10
|
+
export declare class SovariaError extends Error {
|
|
11
|
+
/** HTTP status (0 when the request never got a response). */
|
|
12
|
+
readonly statusCode: number;
|
|
13
|
+
/** The server's response body (or the SDK's own explanation). */
|
|
14
|
+
readonly detail: unknown;
|
|
15
|
+
/** Stable category to branch on. */
|
|
16
|
+
readonly code: SovariaErrorCode;
|
|
17
|
+
/** The server code when one was sent (e.g. PLAN_LIMIT_REACHED), else `code`. */
|
|
18
|
+
readonly serverCode: string;
|
|
19
|
+
/** `HTTP <status>: <server text>` — for logs and developers, not for end users. */
|
|
20
|
+
technicalMessage: string;
|
|
21
|
+
/**
|
|
22
|
+
* The request's trace id (W3C traceparent). In the console, Logs › Request traces (or
|
|
23
|
+
* `sov.api.observability…` / `sov logs why <id>`) says which check refused the request and why.
|
|
24
|
+
*/
|
|
25
|
+
traceId?: string;
|
|
26
|
+
constructor(statusCode: number, detail: unknown, code?: SovariaErrorCode);
|
|
27
|
+
/** Attach the request's trace id (kept in `technicalMessage` for logs). */
|
|
28
|
+
withTraceId(traceId: string | null | undefined): this;
|
|
29
|
+
/** A mistake in how the SDK is set up or called; the message is written for the developer. */
|
|
30
|
+
static config(message: string, statusCode?: number): SovariaError;
|
|
31
|
+
/** The message to show your users (same as `message`). */
|
|
32
|
+
get userMessage(): string;
|
|
33
|
+
toString(): string;
|
|
34
|
+
}
|
|
35
|
+
/** A friendly message for any thrown value: SovariaError text as-is, anything else generic. */
|
|
36
|
+
export declare function userMessageFor(error: unknown, fallback?: string): string;
|
|
37
|
+
/** A W3C trace id (32 hex) for one logical request; every retry of it shares the id. */
|
|
38
|
+
export declare function newTraceId(): string;
|
|
39
|
+
/** `traceparent` for one attempt: the request's trace id and a fresh span id. */
|
|
40
|
+
export declare function traceparentFor(traceId: string): string;
|