@protobase/client 0.7.0 → 0.8.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/dist/auth-types.d.ts +13 -1
- package/dist/auth.d.ts +41 -1
- package/dist/auth.js +6 -2
- package/dist/client.d.ts +35 -0
- package/dist/client.js +42 -1
- package/dist/index.d.ts +6 -4
- package/dist/organizations.d.ts +119 -0
- package/dist/organizations.js +125 -0
- package/dist/static-session.d.ts +1 -1
- package/dist/static-session.js +27 -1
- package/dist/transport.js +4 -3
- package/dist/types.d.ts +14 -0
- package/dist/upload-request.d.ts +4 -0
- package/dist/upload-request.js +30 -0
- package/package.json +3 -3
package/dist/auth-types.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type PlatformSignIn = {
|
|
|
26
26
|
* `needsAdmin` while no user exists yet; `signInMethods` the ways to sign in the server's policy leaves on;
|
|
27
27
|
* `passwordReset` when the server can email reset links; `socialProviders` the ids of the sign-in providers, for example
|
|
28
28
|
* `github`; `platformSignIn` the one of them the platform adds; `staffSignIn` the operator provider's name when its staff
|
|
29
|
-
* can sign in as people.
|
|
29
|
+
* can sign in as people; `organizations` when the app has them.
|
|
30
30
|
*/
|
|
31
31
|
export type SetupStatus = {
|
|
32
32
|
needsAdmin: boolean;
|
|
@@ -35,5 +35,17 @@ export type SetupStatus = {
|
|
|
35
35
|
socialProviders: string[];
|
|
36
36
|
platformSignIn?: PlatformSignIn;
|
|
37
37
|
staffSignIn?: string;
|
|
38
|
+
/** With organizations: who may create them, and every role with its label, `membership` for those a member can hold. */
|
|
39
|
+
organizations?: {
|
|
40
|
+
create: 'admins' | 'everyone';
|
|
41
|
+
roles: RoleLabel[];
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/** A role with its label; `membership` when a member can hold it, `grants` the other roles its holders may give. */
|
|
45
|
+
export type RoleLabel = {
|
|
46
|
+
name: string;
|
|
47
|
+
label: string;
|
|
48
|
+
membership: boolean;
|
|
49
|
+
grants: string[];
|
|
38
50
|
};
|
|
39
51
|
//#endregion
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SignInPolicy, SignInPolicyState } from "./sign-in-policy.js";
|
|
2
2
|
import { AccountSignIn, AuthenticatorSetup, Passkey } from "./account-security.js";
|
|
3
|
+
import { CurrentOrganization, InvitationPreview, InvitationSent, Organization, OrganizationInvitation, OrganizationMember, OrganizationRole } from "./organizations.js";
|
|
3
4
|
import { StaffSignIn } from "./staff-sign-in.js";
|
|
4
5
|
import { AuthUser, SetupStatus, SignInResult, TwoFactorMethod } from "./auth-types.js";
|
|
5
6
|
import { AuthError } from "./auth-error.js";
|
|
@@ -20,7 +21,8 @@ export declare const tokenExpiry: (token: string) => number;
|
|
|
20
21
|
/**
|
|
21
22
|
* The browser side of Better Auth: sign in with a password, an emailed code or a passkey (with a second step for an
|
|
22
23
|
* account with two-factor authentication), the account's passkeys and two-factor authentication (`account`), the
|
|
23
|
-
* sign-in policy for admins (`signInPolicy`), staff of the operator signing in as people (`staff`),
|
|
24
|
+
* sign-in policy for admins (`signInPolicy`), staff of the operator signing in as people (`staff`), organizations
|
|
25
|
+
* (`organizations`), and a short-lived
|
|
24
26
|
* JWT held in memory only (never in storage), fetched with the session cookie and refreshed before it expires.
|
|
25
27
|
*/
|
|
26
28
|
export declare const createAuthSession: (options?: AuthSessionOptions) => {
|
|
@@ -52,6 +54,44 @@ export declare const createAuthSession: (options?: AuthSessionOptions) => {
|
|
|
52
54
|
linkProvider: (provider: string, callbackURL: string) => Promise<string>;
|
|
53
55
|
newBackupCodes: (password?: string) => Promise<string[]>;
|
|
54
56
|
};
|
|
57
|
+
organizations: {
|
|
58
|
+
list: () => Promise<Organization[]>;
|
|
59
|
+
current: () => Promise<CurrentOrganization>;
|
|
60
|
+
switchTo: (organizationId: string | null) => Promise<{
|
|
61
|
+
organizationId: string | null;
|
|
62
|
+
}>;
|
|
63
|
+
search: (query: string) => Promise<Organization[]>;
|
|
64
|
+
create: (input: {
|
|
65
|
+
name: string;
|
|
66
|
+
slug: string;
|
|
67
|
+
}) => Promise<Organization>;
|
|
68
|
+
rename: (organizationId: string, data: {
|
|
69
|
+
name?: string;
|
|
70
|
+
slug?: string;
|
|
71
|
+
}) => Promise<Organization>;
|
|
72
|
+
remove: (organizationId: string) => Promise<Organization>;
|
|
73
|
+
leave: (organizationId: string) => Promise<unknown>;
|
|
74
|
+
members: (organizationId: string) => Promise<OrganizationMember[]>;
|
|
75
|
+
setRole: (organizationId: string, memberId: string, role: OrganizationRole) => Promise<unknown>;
|
|
76
|
+
setAppRoles: (memberId: string, appRoles: string[]) => Promise<unknown>;
|
|
77
|
+
removeMember: (organizationId: string, memberId: string) => Promise<unknown>;
|
|
78
|
+
transferOwnership: (memberId: string, keepAs?: "admin" | "member") => Promise<unknown>;
|
|
79
|
+
invitations: (organizationId: string) => Promise<OrganizationInvitation[]>;
|
|
80
|
+
invite: (organizationId: string, input: {
|
|
81
|
+
email: string;
|
|
82
|
+
role: OrganizationRole;
|
|
83
|
+
appRoles: string[];
|
|
84
|
+
}) => Promise<InvitationSent>;
|
|
85
|
+
resend: (organizationId: string, sent: Pick<OrganizationInvitation, "email" | "role">) => Promise<InvitationSent>;
|
|
86
|
+
revoke: (invitationId: string) => Promise<unknown>;
|
|
87
|
+
myInvitations: () => Promise<OrganizationInvitation[]>;
|
|
88
|
+
invitation: (token: string) => Promise<InvitationPreview>;
|
|
89
|
+
accept: (token: string) => Promise<unknown>;
|
|
90
|
+
signUpFromInvitation: (token: string, input: {
|
|
91
|
+
name: string;
|
|
92
|
+
password?: string;
|
|
93
|
+
}) => Promise<unknown>;
|
|
94
|
+
};
|
|
55
95
|
signInPolicy: {
|
|
56
96
|
read: () => Promise<SignInPolicyState>;
|
|
57
97
|
save: (policy: SignInPolicy) => Promise<SignInPolicyState>;
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AuthError, authError } from "./auth-error.js";
|
|
2
2
|
import { accountSecurity } from "./account-security.js";
|
|
3
3
|
import { betterAuthClient } from "./better-auth-client.js";
|
|
4
|
+
import { organizationsClient } from "./organizations.js";
|
|
4
5
|
import { signInFlows } from "./sign-in-flows.js";
|
|
5
6
|
import { signInPolicyClient } from "./sign-in-policy.js";
|
|
6
7
|
import { staffSignInClient } from "./staff-sign-in.js";
|
|
@@ -16,7 +17,8 @@ const tokenExpiry = (token) => {
|
|
|
16
17
|
/**
|
|
17
18
|
* The browser side of Better Auth: sign in with a password, an emailed code or a passkey (with a second step for an
|
|
18
19
|
* account with two-factor authentication), the account's passkeys and two-factor authentication (`account`), the
|
|
19
|
-
* sign-in policy for admins (`signInPolicy`), staff of the operator signing in as people (`staff`),
|
|
20
|
+
* sign-in policy for admins (`signInPolicy`), staff of the operator signing in as people (`staff`), organizations
|
|
21
|
+
* (`organizations`), and a short-lived
|
|
20
22
|
* JWT held in memory only (never in storage), fetched with the session cookie and refreshed before it expires.
|
|
21
23
|
*/
|
|
22
24
|
const createAuthSession = (options = {}) => {
|
|
@@ -72,7 +74,8 @@ const createAuthSession = (options = {}) => {
|
|
|
72
74
|
passwordReset: Boolean(body.passwordReset),
|
|
73
75
|
socialProviders: body.socialProviders ?? [],
|
|
74
76
|
...body.platformSignIn && { platformSignIn: body.platformSignIn },
|
|
75
|
-
...body.staffSignIn && { staffSignIn: body.staffSignIn }
|
|
77
|
+
...body.staffSignIn && { staffSignIn: body.staffSignIn },
|
|
78
|
+
...body.organizations && { organizations: body.organizations }
|
|
76
79
|
};
|
|
77
80
|
},
|
|
78
81
|
/** The signed-in user from the session cookie, or `undefined`. */
|
|
@@ -120,6 +123,7 @@ const createAuthSession = (options = {}) => {
|
|
|
120
123
|
if (error) throw authError(error, "Could not set the password");
|
|
121
124
|
},
|
|
122
125
|
account: accountSecurity(client),
|
|
126
|
+
organizations: organizationsClient(client, () => token({ refresh: true })),
|
|
123
127
|
signInPolicy: signInPolicyClient(client),
|
|
124
128
|
staff: {
|
|
125
129
|
...staff,
|
package/dist/client.d.ts
CHANGED
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
import { FilterInput, ListParams } from "./query-string.js";
|
|
2
2
|
import { ClientOptions } from "./transport.js";
|
|
3
|
+
import { UploadProgress } from "./upload-request.js";
|
|
3
4
|
import { BatchOp, BatchResult, Facet, Histogram, ListPage, MetaResult, RecordOf, ResourceRef, Series, SeriesParams, Stored } from "./types.js";
|
|
4
5
|
import { KeyValue } from "@protobase/schema";
|
|
5
6
|
//#region src/client.d.ts
|
|
7
|
+
/** How `invoke` calls a function: GET without a body, POST with one, unless `method` says otherwise. */
|
|
8
|
+
export type InvokeOptions = {
|
|
9
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
10
|
+
/** Sent as JSON. */
|
|
11
|
+
body?: unknown;
|
|
12
|
+
/** Below the function, such as `/42` for `/api/functions/<name>/42`. */
|
|
13
|
+
path?: string;
|
|
14
|
+
query?: Record<string, string>;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
/** What `upload` answers: `value` is the ticket to send as the field's value in a create, update or batch write. */
|
|
18
|
+
export type UploadResult = {
|
|
19
|
+
value: string;
|
|
20
|
+
/** The type is the one detected from the content. */
|
|
21
|
+
file: {
|
|
22
|
+
name: string;
|
|
23
|
+
type: string;
|
|
24
|
+
size: number;
|
|
25
|
+
};
|
|
26
|
+
/** Values the field's processors computed for other fields; the write stores them with the file. */
|
|
27
|
+
derived: Record<string, unknown>;
|
|
28
|
+
/** Set when the content's type differs from what the name or browser said: `photo.png` that is a JPEG. */
|
|
29
|
+
corrected?: {
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export type UploadOptions = {
|
|
35
|
+
/** Reports bytes sent; uses XMLHttpRequest where there is one, since fetch reports no upload progress. */
|
|
36
|
+
onProgress?: UploadProgress;
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
};
|
|
6
39
|
export declare const createClient: (options?: ClientOptions) => {
|
|
7
40
|
list: <R extends ResourceRef>(resource: R, params?: ListParams) => Promise<ListPage<RecordOf<R>>>;
|
|
8
41
|
search: <R extends ResourceRef>(resource: R, params?: ListParams) => Promise<ListPage<RecordOf<R>>>;
|
|
@@ -32,6 +65,8 @@ export declare const createClient: (options?: ClientOptions) => {
|
|
|
32
65
|
position: number;
|
|
33
66
|
}) => Promise<ListPage<RecordOf<R>>>;
|
|
34
67
|
meta: (etag?: string) => Promise<MetaResult>;
|
|
68
|
+
invoke: <T = unknown>(name: string, { method, body, path, query, headers }?: InvokeOptions) => Promise<T>;
|
|
69
|
+
upload: (resource: ResourceRef, field: string, file: Blob, { onProgress, signal }?: UploadOptions) => Promise<UploadResult>;
|
|
35
70
|
};
|
|
36
71
|
export type Client = ReturnType<typeof createClient>;
|
|
37
72
|
//#endregion
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { filterText, listWire, queryString } from "./query-string.js";
|
|
2
2
|
import { createTransport } from "./transport.js";
|
|
3
|
+
import { fetchWithProgress } from "./upload-request.js";
|
|
3
4
|
import { encodeKey } from "@protobase/schema";
|
|
4
5
|
//#region src/client.ts
|
|
5
6
|
const nameOf = (resource) => typeof resource === "string" ? resource : resource.toModel().name;
|
|
@@ -185,6 +186,44 @@ const createClient = (options = {}) => {
|
|
|
185
186
|
etag: etagOf(response)
|
|
186
187
|
};
|
|
187
188
|
};
|
|
189
|
+
/**
|
|
190
|
+
* Calls one of the app's API functions, `/api/functions/<name>`, with the user's token. Resolves to its answer: parsed
|
|
191
|
+
* JSON, text, or `undefined` for a 204. A problem rejects as from any other call.
|
|
192
|
+
*/
|
|
193
|
+
const invoke = async (name, { method, body, path = "", query, headers } = {}) => {
|
|
194
|
+
const response = await send({
|
|
195
|
+
method: method ?? (body === void 0 ? "GET" : "POST"),
|
|
196
|
+
path: `/functions/${encodeURIComponent(name)}${path}`,
|
|
197
|
+
query: query ? `?${new URLSearchParams(query)}` : "",
|
|
198
|
+
body,
|
|
199
|
+
headers,
|
|
200
|
+
system: true
|
|
201
|
+
});
|
|
202
|
+
if (response.status === 204) return void 0;
|
|
203
|
+
return response.headers.get("content-type")?.includes("json") ? response.json() : response.text();
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Uploads a file for a file field (`POST /{resource}:upload`). Send `value` of the result as the field's value in a
|
|
207
|
+
* create, update or batch write within 24 hours; the server detects the type and refuses one the field does not take.
|
|
208
|
+
*/
|
|
209
|
+
const upload = async (resource, field, file, { onProgress, signal } = {}) => {
|
|
210
|
+
const name = "name" in file && typeof file.name === "string" ? file.name : void 0;
|
|
211
|
+
const progress = onProgress && typeof XMLHttpRequest !== "undefined" ? fetchWithProgress(onProgress) : void 0;
|
|
212
|
+
const result = await (await send({
|
|
213
|
+
method: "POST",
|
|
214
|
+
path: `/${nameOf(resource)}:upload`,
|
|
215
|
+
query: `?${new URLSearchParams({
|
|
216
|
+
field,
|
|
217
|
+
...name !== void 0 && { name }
|
|
218
|
+
})}`,
|
|
219
|
+
raw: file,
|
|
220
|
+
headers: { "content-type": file.type || "application/octet-stream" },
|
|
221
|
+
...progress && { via: progress },
|
|
222
|
+
...signal && { signal }
|
|
223
|
+
})).json();
|
|
224
|
+
onProgress?.(file.size, file.size);
|
|
225
|
+
return result;
|
|
226
|
+
};
|
|
188
227
|
return {
|
|
189
228
|
list,
|
|
190
229
|
search,
|
|
@@ -199,7 +238,9 @@ const createClient = (options = {}) => {
|
|
|
199
238
|
series,
|
|
200
239
|
histogram,
|
|
201
240
|
seek,
|
|
202
|
-
meta
|
|
241
|
+
meta,
|
|
242
|
+
invoke,
|
|
243
|
+
upload
|
|
203
244
|
};
|
|
204
245
|
};
|
|
205
246
|
//#endregion
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { FilterInput, ListParams, filterText, listWire } from "./query-string.js";
|
|
2
2
|
import { ClientOptions } from "./transport.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { UploadProgress } from "./upload-request.js";
|
|
4
|
+
import { BatchOp, BatchRef, BatchResult, Facet, Histogram, HistogramBucket, ListPage, Meta, MetaResult, RecordOf, RecordPermissions, ResourcePermissions, ResourceRef, Series, SeriesParams, SeriesPoint, Stored, StoredFile } from "./types.js";
|
|
5
|
+
import { Client, InvokeOptions, UploadOptions, UploadResult, createClient } from "./client.js";
|
|
5
6
|
import { ApiError, PreconditionFailedError, ProblemDetails, ProblemError, isApiError } from "./problem.js";
|
|
6
7
|
import { SignInPolicy, SignInPolicyClient, SignInPolicyState, SignInRule, StaffAccessRule } from "./sign-in-policy.js";
|
|
7
8
|
import { AccountSecurity, AccountSignIn, AuthenticatorSetup, Passkey, RequiredSetup } from "./account-security.js";
|
|
9
|
+
import { CurrentOrganization, InvitationPreview, InvitationSent, Organization, OrganizationInvitation, OrganizationMember, OrganizationRole, OrganizationsClient } from "./organizations.js";
|
|
8
10
|
import { StaffSignIn, StaffSignInClient } from "./staff-sign-in.js";
|
|
9
|
-
import { AuthUser, PlatformSignIn, SetupStatus, SignInMethod, SignInResult, TwoFactorMethod } from "./auth-types.js";
|
|
11
|
+
import { AuthUser, PlatformSignIn, RoleLabel, SetupStatus, SignInMethod, SignInResult, TwoFactorMethod } from "./auth-types.js";
|
|
10
12
|
import { AuthError } from "./auth-error.js";
|
|
11
13
|
import { AuthSession, AuthSessionOptions, createAuthSession, tokenExpiry } from "./auth.js";
|
|
12
14
|
import { createStaticSession } from "./static-session.js";
|
|
13
15
|
import { AssistantClient, AssistantClientOptions, AssistantConnection, createAssistantClient } from "./assistant.js";
|
|
14
16
|
import { RuntimeClient, RuntimeClientOptions, RuntimePolicy, RuntimeStatus, createRuntimeClient } from "./runtime.js";
|
|
15
|
-
export { type AccountSecurity, type AccountSignIn, ApiError, type AssistantClient, type AssistantClientOptions, type AssistantConnection, AuthError, type AuthSession, type AuthSessionOptions, type AuthUser, type AuthenticatorSetup, type BatchOp, type BatchRef, type BatchResult, type Client, type ClientOptions, type Facet, type FilterInput, type Histogram, type HistogramBucket, type ListPage, type ListParams, type Meta, type MetaResult, type Passkey, type PlatformSignIn, PreconditionFailedError, type ProblemDetails, type ProblemError, type RecordOf, type RecordPermissions, type RequiredSetup, type ResourcePermissions, type ResourceRef, type RuntimeClient, type RuntimeClientOptions, type RuntimePolicy, type RuntimeStatus, type Series, type SeriesParams, type SeriesPoint, type SetupStatus, type SignInMethod, type SignInPolicy, type SignInPolicyClient, type SignInPolicyState, type SignInResult, type SignInRule, type StaffAccessRule, type StaffSignIn, type StaffSignInClient, type Stored, type TwoFactorMethod, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, filterText, isApiError, listWire, tokenExpiry };
|
|
17
|
+
export { type AccountSecurity, type AccountSignIn, ApiError, type AssistantClient, type AssistantClientOptions, type AssistantConnection, AuthError, type AuthSession, type AuthSessionOptions, type AuthUser, type AuthenticatorSetup, type BatchOp, type BatchRef, type BatchResult, type Client, type ClientOptions, type CurrentOrganization, type Facet, type FilterInput, type Histogram, type HistogramBucket, type InvitationPreview, type InvitationSent, type InvokeOptions, type ListPage, type ListParams, type Meta, type MetaResult, type Organization, type OrganizationInvitation, type OrganizationMember, type OrganizationRole, type OrganizationsClient, type Passkey, type PlatformSignIn, PreconditionFailedError, type ProblemDetails, type ProblemError, type RecordOf, type RecordPermissions, type RequiredSetup, type ResourcePermissions, type ResourceRef, type RoleLabel, type RuntimeClient, type RuntimeClientOptions, type RuntimePolicy, type RuntimeStatus, type Series, type SeriesParams, type SeriesPoint, type SetupStatus, type SignInMethod, type SignInPolicy, type SignInPolicyClient, type SignInPolicyState, type SignInResult, type SignInRule, type StaffAccessRule, type StaffSignIn, type StaffSignInClient, type Stored, type StoredFile, type TwoFactorMethod, type UploadOptions, type UploadProgress, type UploadResult, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, filterText, isApiError, listWire, tokenExpiry };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { BetterAuthClient } from "./better-auth-client.js";
|
|
2
|
+
//#region src/organizations.d.ts
|
|
3
|
+
/** Who manages an organization: owners everything, admins its members and details, members nothing. */
|
|
4
|
+
export type OrganizationRole = 'owner' | 'admin' | 'member';
|
|
5
|
+
export type Organization = {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
logo?: string | null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The organization the session works in: for a member with their organization role, app roles there and membership
|
|
13
|
+
* id; for someone with a global role who entered it without being a member, without them. `globalRoles` are the
|
|
14
|
+
* person's roles in every organization.
|
|
15
|
+
*/
|
|
16
|
+
export type CurrentOrganization = {
|
|
17
|
+
organization: Organization | null;
|
|
18
|
+
role?: OrganizationRole;
|
|
19
|
+
appRoles?: string[];
|
|
20
|
+
memberId?: string;
|
|
21
|
+
globalRoles: string[];
|
|
22
|
+
};
|
|
23
|
+
export type OrganizationMember = {
|
|
24
|
+
id: string;
|
|
25
|
+
userId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
email: string;
|
|
28
|
+
role: OrganizationRole;
|
|
29
|
+
appRoles: string[];
|
|
30
|
+
createdAt: string;
|
|
31
|
+
};
|
|
32
|
+
/** `organizationName` comes with the person's own invitations, which span organizations. */
|
|
33
|
+
export type OrganizationInvitation = {
|
|
34
|
+
id: string;
|
|
35
|
+
email: string;
|
|
36
|
+
role: OrganizationRole;
|
|
37
|
+
appRoles: string[];
|
|
38
|
+
status: string;
|
|
39
|
+
expiresAt: string;
|
|
40
|
+
organizationName?: string;
|
|
41
|
+
};
|
|
42
|
+
/** An invitation as its link shows it, before it is accepted. */
|
|
43
|
+
export type InvitationPreview = {
|
|
44
|
+
organization: {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
};
|
|
48
|
+
inviter: {
|
|
49
|
+
name: string;
|
|
50
|
+
};
|
|
51
|
+
email: string;
|
|
52
|
+
role: OrganizationRole;
|
|
53
|
+
appRoles: string[];
|
|
54
|
+
expiresAt: string;
|
|
55
|
+
hasAccount: boolean;
|
|
56
|
+
};
|
|
57
|
+
/** What inviting gives back: the invitation, and its link when the server cannot email it. */
|
|
58
|
+
export type InvitationSent = {
|
|
59
|
+
invitation: OrganizationInvitation;
|
|
60
|
+
link?: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The signed-in person's organizations: switching, creating and managing them, their members and invitations, and
|
|
64
|
+
* invitation links. A change of the organization one works in fetches a new token at once (`refreshToken`), since the
|
|
65
|
+
* token names it. Each call rejects with `AuthError`.
|
|
66
|
+
*/
|
|
67
|
+
export declare const organizationsClient: (client: BetterAuthClient, refreshToken: () => Promise<unknown>) => {
|
|
68
|
+
/** The organizations the person is a member of. */
|
|
69
|
+
list: () => Promise<Organization[]>;
|
|
70
|
+
current: () => Promise<CurrentOrganization>;
|
|
71
|
+
/** Works in another organization from now on; `null` for none. */
|
|
72
|
+
switchTo: (organizationId: string | null) => Promise<{
|
|
73
|
+
organizationId: string | null;
|
|
74
|
+
}>;
|
|
75
|
+
/** Every organization whose name or slug contains `query`, for people with a global role. */
|
|
76
|
+
search: (query: string) => Promise<Organization[]>;
|
|
77
|
+
/** Creates an organization, owned by the person with every app role, and works in it. */
|
|
78
|
+
create: (input: {
|
|
79
|
+
name: string;
|
|
80
|
+
slug: string;
|
|
81
|
+
}) => Promise<Organization>;
|
|
82
|
+
rename: (organizationId: string, data: {
|
|
83
|
+
name?: string;
|
|
84
|
+
slug?: string;
|
|
85
|
+
}) => Promise<Organization>;
|
|
86
|
+
/** Deletes an organization; the server refuses one that still has records. */
|
|
87
|
+
remove: (organizationId: string) => Promise<Organization>;
|
|
88
|
+
leave: (organizationId: string) => Promise<unknown>;
|
|
89
|
+
members: (organizationId: string) => Promise<OrganizationMember[]>;
|
|
90
|
+
setRole: (organizationId: string, memberId: string, role: OrganizationRole) => Promise<unknown>;
|
|
91
|
+
/** Sets a member's app roles; only the roles the person may give can change. */
|
|
92
|
+
setAppRoles: (memberId: string, appRoles: string[]) => Promise<unknown>;
|
|
93
|
+
removeMember: (organizationId: string, memberId: string) => Promise<unknown>;
|
|
94
|
+
/** Hands the organization over to a member; the person stays as `keepAs`, an admin unless said otherwise. */
|
|
95
|
+
transferOwnership: (memberId: string, keepAs?: "admin" | "member") => Promise<unknown>;
|
|
96
|
+
invitations: (organizationId: string) => Promise<OrganizationInvitation[]>;
|
|
97
|
+
/** Invites an address; the link comes back only when the server cannot email it. */
|
|
98
|
+
invite: (organizationId: string, input: {
|
|
99
|
+
email: string;
|
|
100
|
+
role: OrganizationRole;
|
|
101
|
+
appRoles: string[];
|
|
102
|
+
}) => Promise<InvitationSent>;
|
|
103
|
+
/** Sends an invitation again with a new link, which voids the earlier one. */
|
|
104
|
+
resend: (organizationId: string, sent: Pick<OrganizationInvitation, "email" | "role">) => Promise<InvitationSent>;
|
|
105
|
+
revoke: (invitationId: string) => Promise<unknown>;
|
|
106
|
+
/** The pending invitations for the person's own address. */
|
|
107
|
+
myInvitations: () => Promise<OrganizationInvitation[]>;
|
|
108
|
+
/** What an invitation link is for; works signed out. */
|
|
109
|
+
invitation: (token: string) => Promise<InvitationPreview>;
|
|
110
|
+
/** Accepts an invitation signed in as its address, and works in its organization. */
|
|
111
|
+
accept: (token: string) => Promise<unknown>;
|
|
112
|
+
/** Creates an account for an invited address without one, accepts the invitation and signs in. */
|
|
113
|
+
signUpFromInvitation: (token: string, input: {
|
|
114
|
+
name: string;
|
|
115
|
+
password?: string;
|
|
116
|
+
}) => Promise<unknown>;
|
|
117
|
+
};
|
|
118
|
+
export type OrganizationsClient = ReturnType<typeof organizationsClient>;
|
|
119
|
+
//#endregion
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { authError } from "./auth-error.js";
|
|
2
|
+
//#region src/organizations.ts
|
|
3
|
+
const rolesOf = (value) => Array.isArray(value) ? value.filter((name) => typeof name === "string") : typeof value === "string" ? JSON.parse(value) : [];
|
|
4
|
+
const organizationRole = (role) => {
|
|
5
|
+
const roles = role.split(",");
|
|
6
|
+
return roles.includes("owner") ? "owner" : roles.includes("admin") ? "admin" : "member";
|
|
7
|
+
};
|
|
8
|
+
const member = (raw) => ({
|
|
9
|
+
id: raw.id,
|
|
10
|
+
userId: raw.userId,
|
|
11
|
+
name: raw.user.name || raw.user.email,
|
|
12
|
+
email: raw.user.email,
|
|
13
|
+
role: organizationRole(raw.role),
|
|
14
|
+
appRoles: rolesOf(raw.appRoles),
|
|
15
|
+
createdAt: new Date(raw.createdAt).toISOString()
|
|
16
|
+
});
|
|
17
|
+
const invitation = (raw) => ({
|
|
18
|
+
id: raw.id,
|
|
19
|
+
email: raw.email,
|
|
20
|
+
role: organizationRole(raw.role),
|
|
21
|
+
appRoles: rolesOf(raw.appRoles),
|
|
22
|
+
status: raw.status,
|
|
23
|
+
expiresAt: new Date(raw.expiresAt).toISOString(),
|
|
24
|
+
...raw.organizationName && { organizationName: raw.organizationName }
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* The signed-in person's organizations: switching, creating and managing them, their members and invitations, and
|
|
28
|
+
* invitation links. A change of the organization one works in fetches a new token at once (`refreshToken`), since the
|
|
29
|
+
* token names it. Each call rejects with `AuthError`.
|
|
30
|
+
*/
|
|
31
|
+
const organizationsClient = (client, refreshToken) => {
|
|
32
|
+
const call = async (path, fallback, init = {}) => {
|
|
33
|
+
const { data, error } = await client.$fetch(path, {
|
|
34
|
+
method: init.method ?? (init.body ? "POST" : "GET"),
|
|
35
|
+
...init.body && { body: init.body },
|
|
36
|
+
...init.query && { query: init.query }
|
|
37
|
+
});
|
|
38
|
+
if (error) throw authError(error, fallback);
|
|
39
|
+
return data;
|
|
40
|
+
};
|
|
41
|
+
const thenRefresh = async (result) => {
|
|
42
|
+
await refreshToken();
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
/** The organizations the person is a member of. */
|
|
47
|
+
list: async () => await call("/organization/list", "Could not read your organizations") ?? [],
|
|
48
|
+
current: () => call("/organization/current", "Could not read the current organization"),
|
|
49
|
+
/** Works in another organization from now on; `null` for none. */
|
|
50
|
+
switchTo: async (organizationId) => thenRefresh(await call("/organization/switch", "Could not switch the organization", { body: { organizationId } })),
|
|
51
|
+
/** Every organization whose name or slug contains `query`, for people with a global role. */
|
|
52
|
+
search: async (query) => (await call("/organization/search", "Could not search the organizations", { query: { query } })).organizations,
|
|
53
|
+
/** Creates an organization, owned by the person with every app role, and works in it. */
|
|
54
|
+
create: async (input) => thenRefresh(await call("/organization/create", "Could not create the organization", { body: input })),
|
|
55
|
+
rename: (organizationId, data) => call("/organization/update", "Could not change the organization", { body: {
|
|
56
|
+
organizationId,
|
|
57
|
+
data
|
|
58
|
+
} }),
|
|
59
|
+
/** Deletes an organization; the server refuses one that still has records. */
|
|
60
|
+
remove: async (organizationId) => thenRefresh(await call("/organization/delete", "Could not delete the organization", { body: { organizationId } })),
|
|
61
|
+
leave: async (organizationId) => thenRefresh(await call("/organization/leave", "Could not leave the organization", { body: { organizationId } })),
|
|
62
|
+
members: async (organizationId) => (await call("/organization/list-members", "Could not read the members", { query: {
|
|
63
|
+
organizationId,
|
|
64
|
+
limit: "1000"
|
|
65
|
+
} })).members.map(member),
|
|
66
|
+
setRole: (organizationId, memberId, role) => call("/organization/update-member-role", "Could not change the role", { body: {
|
|
67
|
+
organizationId,
|
|
68
|
+
memberId,
|
|
69
|
+
role
|
|
70
|
+
} }),
|
|
71
|
+
/** Sets a member's app roles; only the roles the person may give can change. */
|
|
72
|
+
setAppRoles: (memberId, appRoles) => call("/organization/set-app-roles", "Could not change the app roles", { body: {
|
|
73
|
+
memberId,
|
|
74
|
+
appRoles
|
|
75
|
+
} }),
|
|
76
|
+
removeMember: (organizationId, memberId) => call("/organization/remove-member", "Could not remove the member", { body: {
|
|
77
|
+
organizationId,
|
|
78
|
+
memberIdOrEmail: memberId
|
|
79
|
+
} }),
|
|
80
|
+
/** Hands the organization over to a member; the person stays as `keepAs`, an admin unless said otherwise. */
|
|
81
|
+
transferOwnership: (memberId, keepAs = "admin") => call("/organization/transfer-ownership", "Could not hand the organization over", { body: {
|
|
82
|
+
memberId,
|
|
83
|
+
keepAs
|
|
84
|
+
} }),
|
|
85
|
+
invitations: async (organizationId) => (await call("/organization/list-invitations", "Could not read the invitations", { query: { organizationId } }) ?? []).map(invitation),
|
|
86
|
+
/** Invites an address; the link comes back only when the server cannot email it. */
|
|
87
|
+
invite: async (organizationId, input) => {
|
|
88
|
+
const raw = await call("/organization/invite-member", "Could not send the invitation", { body: {
|
|
89
|
+
organizationId,
|
|
90
|
+
...input
|
|
91
|
+
} });
|
|
92
|
+
return {
|
|
93
|
+
invitation: invitation(raw),
|
|
94
|
+
...raw.link && { link: raw.link }
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
/** Sends an invitation again with a new link, which voids the earlier one. */
|
|
98
|
+
resend: async (organizationId, sent) => {
|
|
99
|
+
const raw = await call("/organization/invite-member", "Could not send the invitation again", { body: {
|
|
100
|
+
organizationId,
|
|
101
|
+
email: sent.email,
|
|
102
|
+
role: sent.role,
|
|
103
|
+
resend: true
|
|
104
|
+
} });
|
|
105
|
+
return {
|
|
106
|
+
invitation: invitation(raw),
|
|
107
|
+
...raw.link && { link: raw.link }
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
revoke: (invitationId) => call("/organization/cancel-invitation", "Could not revoke the invitation", { body: { invitationId } }),
|
|
111
|
+
/** The pending invitations for the person's own address. */
|
|
112
|
+
myInvitations: async () => (await call("/organization/list-user-invitations", "Could not read your invitations") ?? []).map(invitation),
|
|
113
|
+
/** What an invitation link is for; works signed out. */
|
|
114
|
+
invitation: (token) => call("/invitation", "This invitation link is not valid", { query: { token } }),
|
|
115
|
+
/** Accepts an invitation signed in as its address, and works in its organization. */
|
|
116
|
+
accept: async (token) => thenRefresh(await call("/invitation/accept", "Could not accept the invitation", { body: { token } })),
|
|
117
|
+
/** Creates an account for an invited address without one, accepts the invitation and signs in. */
|
|
118
|
+
signUpFromInvitation: async (token, input) => thenRefresh(await call("/invitation/sign-up", "Could not create your account", { body: {
|
|
119
|
+
token,
|
|
120
|
+
...input
|
|
121
|
+
} }))
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
export { organizationsClient };
|
package/dist/static-session.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { AuthSession } from "./auth.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* A session that is already signed in with a token you hold, for stories and scripts (`protobase token <email>`).
|
|
6
6
|
* It never refreshes, so use a token that outlives the run. Its account has nothing to set up, and it cannot change
|
|
7
|
-
* passkeys, two-factor authentication
|
|
7
|
+
* passkeys, two-factor authentication, the sign-in policy or organizations, and is never a staff session.
|
|
8
8
|
*/
|
|
9
9
|
export declare const createStaticSession: (token: string, user?: AuthUser) => AuthSession;
|
|
10
10
|
//#endregion
|
package/dist/static-session.js
CHANGED
|
@@ -6,7 +6,7 @@ const unsupported = (what) => async () => {
|
|
|
6
6
|
/**
|
|
7
7
|
* A session that is already signed in with a token you hold, for stories and scripts (`protobase token <email>`).
|
|
8
8
|
* It never refreshes, so use a token that outlives the run. Its account has nothing to set up, and it cannot change
|
|
9
|
-
* passkeys, two-factor authentication
|
|
9
|
+
* passkeys, two-factor authentication, the sign-in policy or organizations, and is never a staff session.
|
|
10
10
|
*/
|
|
11
11
|
const createStaticSession = (token, user = {
|
|
12
12
|
id: "token",
|
|
@@ -78,6 +78,32 @@ const createStaticSession = (token, user = {
|
|
|
78
78
|
current: async () => void 0,
|
|
79
79
|
stop: unsupported("end a staff session"),
|
|
80
80
|
log: unsupported("read the staff sign-ins")
|
|
81
|
+
},
|
|
82
|
+
organizations: {
|
|
83
|
+
list: async () => [],
|
|
84
|
+
current: async () => ({
|
|
85
|
+
organization: null,
|
|
86
|
+
globalRoles: []
|
|
87
|
+
}),
|
|
88
|
+
switchTo: unsupported("switch organizations"),
|
|
89
|
+
search: async () => [],
|
|
90
|
+
create: unsupported("create an organization"),
|
|
91
|
+
rename: unsupported("change an organization"),
|
|
92
|
+
remove: unsupported("delete an organization"),
|
|
93
|
+
leave: unsupported("leave an organization"),
|
|
94
|
+
members: async () => [],
|
|
95
|
+
setRole: unsupported("change a role"),
|
|
96
|
+
setAppRoles: unsupported("change app roles"),
|
|
97
|
+
removeMember: unsupported("remove a member"),
|
|
98
|
+
transferOwnership: unsupported("hand an organization over"),
|
|
99
|
+
invitations: async () => [],
|
|
100
|
+
invite: unsupported("invite people"),
|
|
101
|
+
resend: unsupported("invite people"),
|
|
102
|
+
revoke: unsupported("revoke an invitation"),
|
|
103
|
+
myInvitations: async () => [],
|
|
104
|
+
invitation: unsupported("read an invitation"),
|
|
105
|
+
accept: unsupported("accept an invitation"),
|
|
106
|
+
signUpFromInvitation: unsupported("accept an invitation")
|
|
81
107
|
}
|
|
82
108
|
});
|
|
83
109
|
//#endregion
|
package/dist/transport.js
CHANGED
|
@@ -4,10 +4,10 @@ const createTransport = (options = {}) => {
|
|
|
4
4
|
const baseUrl = options.baseUrl ?? "/api/v1";
|
|
5
5
|
const systemUrl = baseUrl.replace(/\/[^/]*$/, "");
|
|
6
6
|
const doFetch = options.fetch ?? ((input, init) => fetch(input, init));
|
|
7
|
-
return async ({ method, path, query = "", body, headers, accept = [], system }) => {
|
|
7
|
+
return async ({ method, path, query = "", body, headers, accept = [], system, raw, via, signal }) => {
|
|
8
8
|
const attempt = async (refresh) => {
|
|
9
9
|
const token = await options.token?.({ refresh });
|
|
10
|
-
return doFetch(`${system ? systemUrl : baseUrl}${path}${query}`, {
|
|
10
|
+
return (via ?? doFetch)(`${system ? systemUrl : baseUrl}${path}${query}`, {
|
|
11
11
|
method,
|
|
12
12
|
headers: {
|
|
13
13
|
...body !== void 0 && { "content-type": "application/json" },
|
|
@@ -15,7 +15,8 @@ const createTransport = (options = {}) => {
|
|
|
15
15
|
...options.headers?.(),
|
|
16
16
|
...headers
|
|
17
17
|
},
|
|
18
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
18
|
+
body: raw ?? (body === void 0 ? void 0 : JSON.stringify(body)),
|
|
19
|
+
...signal && { signal }
|
|
19
20
|
});
|
|
20
21
|
};
|
|
21
22
|
let response = await attempt(false);
|
package/dist/types.d.ts
CHANGED
|
@@ -97,6 +97,20 @@ export type Stored<T> = {
|
|
|
97
97
|
etag: string;
|
|
98
98
|
permissions?: RecordPermissions;
|
|
99
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* A file field as records carry it. `url` downloads it: signed and valid one to two hours for a private file, permanent
|
|
102
|
+
* for a public one. Send `uri` back unchanged to keep the file, or `null` to clear it.
|
|
103
|
+
*/
|
|
104
|
+
export type StoredFile = {
|
|
105
|
+
uri: string;
|
|
106
|
+
name: string;
|
|
107
|
+
type: string;
|
|
108
|
+
size?: number;
|
|
109
|
+
url?: string;
|
|
110
|
+
} | {
|
|
111
|
+
uri: string;
|
|
112
|
+
missing: true;
|
|
113
|
+
};
|
|
100
114
|
/** A reference to a record created earlier in the same batch (`ref` on its `create`). */
|
|
101
115
|
export type BatchRef = {
|
|
102
116
|
$ref: string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/upload-request.ts
|
|
2
|
+
const headersOf = (raw) => new Headers(raw.trim().split(/[\r\n]+/).filter(Boolean).map((line) => {
|
|
3
|
+
const colon = line.indexOf(":");
|
|
4
|
+
return [line.slice(0, colon).trim(), line.slice(colon + 1).trim()];
|
|
5
|
+
}));
|
|
6
|
+
/**
|
|
7
|
+
* A `fetch` that reports upload progress, through XMLHttpRequest since fetch cannot. It answers with an ordinary
|
|
8
|
+
* Response; a network failure rejects with a TypeError, as fetch does.
|
|
9
|
+
*/
|
|
10
|
+
const fetchWithProgress = (onProgress) => (input, init = {}) => new Promise((resolve, reject) => {
|
|
11
|
+
const request = new XMLHttpRequest();
|
|
12
|
+
request.open(init.method ?? "GET", String(input));
|
|
13
|
+
new Headers(init.headers).forEach((value, name) => request.setRequestHeader(name, value));
|
|
14
|
+
request.responseType = "blob";
|
|
15
|
+
request.upload.onprogress = (event) => onProgress(event.loaded, event.lengthComputable ? event.total : event.loaded);
|
|
16
|
+
request.onload = () => {
|
|
17
|
+
const empty = request.status === 204 || request.status === 304;
|
|
18
|
+
resolve(new Response(empty ? null : request.response, {
|
|
19
|
+
status: request.status,
|
|
20
|
+
statusText: request.statusText,
|
|
21
|
+
headers: headersOf(request.getAllResponseHeaders())
|
|
22
|
+
}));
|
|
23
|
+
};
|
|
24
|
+
request.onerror = () => reject(/* @__PURE__ */ new TypeError("Network request failed"));
|
|
25
|
+
request.onabort = () => reject(new DOMException("The upload was aborted", "AbortError"));
|
|
26
|
+
init.signal?.addEventListener("abort", () => request.abort());
|
|
27
|
+
request.send(init.body);
|
|
28
|
+
});
|
|
29
|
+
//#endregion
|
|
30
|
+
export { fetchWithProgress };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Typed fetch client for the Protobase REST API.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@better-auth/passkey": "^1.7.7",
|
|
33
|
-
"@protobase/layout": "0.
|
|
34
|
-
"@protobase/schema": "0.
|
|
33
|
+
"@protobase/layout": "0.8.0",
|
|
34
|
+
"@protobase/schema": "0.8.0",
|
|
35
35
|
"better-auth": "^1.7.7"
|
|
36
36
|
}
|
|
37
37
|
}
|