@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.
@@ -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`), and a short-lived
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`), and a short-lived
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 { BatchOp, BatchRef, BatchResult, Facet, Histogram, HistogramBucket, ListPage, Meta, MetaResult, RecordOf, RecordPermissions, ResourcePermissions, ResourceRef, Series, SeriesParams, SeriesPoint, Stored } from "./types.js";
4
- import { Client, createClient } from "./client.js";
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 };
@@ -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 or the sign-in policy, and is never a staff session.
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
@@ -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 or the sign-in policy, and is never a staff session.
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,4 @@
1
+ //#region src/upload-request.d.ts
2
+ /** Bytes sent so far, of `total`. */
3
+ export type UploadProgress = (sent: number, total: number) => void;
4
+ //#endregion
@@ -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.7.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.7.0",
34
- "@protobase/schema": "0.7.0",
33
+ "@protobase/layout": "0.8.0",
34
+ "@protobase/schema": "0.8.0",
35
35
  "better-auth": "^1.7.7"
36
36
  }
37
37
  }