@protobase/server 0.21.9 → 0.21.11
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/better-auth/app-tokens.js +1 -1
- package/dist/better-auth/auth-base-path.js +5 -0
- package/dist/better-auth/create-auth.d.ts +1248 -248
- package/dist/better-auth/create-auth.js +22 -6
- package/dist/better-auth/platform-sign-in.js +4 -3
- package/dist/better-auth/saml-sign-in.d.ts +37 -0
- package/dist/better-auth/saml-sign-in.js +133 -0
- package/dist/better-auth/status-route.js +2 -1
- package/dist/check-views.js +10 -1
- package/dist/create-admin.js +1 -1
- package/dist/view-access.js +8 -5
- package/package.json +8 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { multiHostBaseURL, readOrigins, resolveOrigins } from "./origins.js";
|
|
2
2
|
import { defaultAppName, readAppName } from "../app-name.js";
|
|
3
|
+
import { authBasePath } from "./auth-base-path.js";
|
|
3
4
|
import { platformMailer } from "../mail/platform-mailer.js";
|
|
4
5
|
import { readOperatorSettings, resolveOperator } from "./operator-provider.js";
|
|
5
6
|
import { roleDefinitions } from "./organizations/role-definitions.js";
|
|
@@ -15,6 +16,7 @@ import { narrowClaims } from "./roles/active-role.js";
|
|
|
15
16
|
import { rolesPlugin } from "./roles/roles-plugin.js";
|
|
16
17
|
import { platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn } from "./platform-sign-in.js";
|
|
17
18
|
import { signInPolicyPlugin } from "./policy-plugin.js";
|
|
19
|
+
import { emailInDomains, resolveSamlProviders, samlDisabledPaths, samlGuardPlugin, samlPlugin } from "./saml-sign-in.js";
|
|
18
20
|
import { emailCodeOptions, twoFactorCodeOptions, unusedEmailCodePaths } from "./sign-in-mail.js";
|
|
19
21
|
import { signUpPaths, signUpPlugin } from "./sign-up/sign-up-plugin.js";
|
|
20
22
|
import { staffSignInPlugin } from "./staff-plugin.js";
|
|
@@ -30,7 +32,6 @@ const platformOrigins = () => readOrigins(globalThis.process?.env ?? {});
|
|
|
30
32
|
const platformPasskeyDomain = () => readPasskeyDomain(globalThis.process?.env ?? {});
|
|
31
33
|
const platformAppName = () => readAppName(globalThis.process?.env ?? {});
|
|
32
34
|
const tokenLifetime = "15m";
|
|
33
|
-
const authBasePath = "/api/auth";
|
|
34
35
|
/**
|
|
35
36
|
* Better Auth for a single-tenant admin: sign-in with a password, an emailed code or a passkey, two-factor
|
|
36
37
|
* authentication, and the sign-in policy admins set; no public sign-up unless `signUp` turns it on (every account is
|
|
@@ -46,7 +47,7 @@ const organizationPaths = ["/organization/set-active", "/organization/accept-inv
|
|
|
46
47
|
* The options `createAuth` gives Better Auth, with the roles, default role, mailer, operator provider and platform
|
|
47
48
|
* sign-in provider already resolved.
|
|
48
49
|
*/
|
|
49
|
-
const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, organizations, context = noContext }) => {
|
|
50
|
+
const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, definitions, defaultRole, appName = defaultAppName, mailer, signUpRole, operator, platformSignIn, saml = [], organizations, context = noContext }) => {
|
|
50
51
|
const secure = options.baseURL.startsWith("https://");
|
|
51
52
|
const signIn = platformSignInFor(platformSignIn, Object.keys(options.socialProviders ?? {}));
|
|
52
53
|
const hosts = passkeyHosts([options.baseURL, ...origins]);
|
|
@@ -55,6 +56,7 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
55
56
|
appName,
|
|
56
57
|
baseURL: options.baseURL
|
|
57
58
|
};
|
|
59
|
+
const samlIds = saml.map((provider) => provider.id);
|
|
58
60
|
return {
|
|
59
61
|
appName,
|
|
60
62
|
database: options.database,
|
|
@@ -126,9 +128,10 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
126
128
|
mail
|
|
127
129
|
})] : [],
|
|
128
130
|
...signIn ? [genericOAuth({ config: [platformSignInProvider(signIn)] })] : [],
|
|
131
|
+
...saml.length > 0 ? [samlPlugin(saml, options.baseURL), samlGuardPlugin(samlIds)] : [],
|
|
129
132
|
signInPolicyPlugin({
|
|
130
133
|
mail: Boolean(mailer),
|
|
131
|
-
socialProviders: Object.keys(options.socialProviders ?? {}),
|
|
134
|
+
socialProviders: [...Object.keys(options.socialProviders ?? {}), ...samlIds],
|
|
132
135
|
...operator && { operator: operator.name },
|
|
133
136
|
...signIn && { platformSignIn: {
|
|
134
137
|
provider: signIn.provider,
|
|
@@ -153,7 +156,8 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
153
156
|
disabledPaths: [
|
|
154
157
|
...unusedEmailCodePaths,
|
|
155
158
|
...adminImpersonationPaths,
|
|
156
|
-
...organizations ? organizationPaths : []
|
|
159
|
+
...organizations ? organizationPaths : [],
|
|
160
|
+
...saml.length > 0 ? samlDisabledPaths : []
|
|
157
161
|
],
|
|
158
162
|
databaseHooks: {
|
|
159
163
|
user: { create: {
|
|
@@ -161,11 +165,16 @@ const betterAuthOptions = (options, { origins = [], passkeyDomain, roles, defini
|
|
|
161
165
|
const given = String(user.role ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
162
166
|
const unknown = given.find((name) => !roles.includes(name));
|
|
163
167
|
if (given.length === 0 || unknown !== void 0) throw new APIError("BAD_REQUEST", { message: `Choose a role for the new user: ${roles.join(", ")}` });
|
|
168
|
+
const samlProvider = context?.path === "/sso/saml2/sp/acs/:providerId" ? saml.find((provider) => provider.id === context.params?.providerId) : void 0;
|
|
169
|
+
if (samlProvider && !emailInDomains(user.email, samlProvider.domains)) throw new APIError("FORBIDDEN", {
|
|
170
|
+
code: "EMAIL_DOMAIN_NOT_ALLOWED",
|
|
171
|
+
message: "This address is not one the identity provider may sign up."
|
|
172
|
+
});
|
|
164
173
|
const withId = options.generateUserId ? {
|
|
165
174
|
...user,
|
|
166
175
|
id: await options.generateUserId()
|
|
167
176
|
} : user;
|
|
168
|
-
return { data: context?.path === "/admin/create-user" ? {
|
|
177
|
+
return { data: context?.path === "/admin/create-user" || samlProvider ? {
|
|
169
178
|
...withId,
|
|
170
179
|
emailVerified: true
|
|
171
180
|
} : withId };
|
|
@@ -235,6 +244,7 @@ const createAuth = (options) => {
|
|
|
235
244
|
const signInSettings = options.signInProvider === void 0 ? platformSignInSettings() : options.signInProvider || void 0;
|
|
236
245
|
const socialProviders = Object.keys(options.socialProviders ?? {});
|
|
237
246
|
const platformSignIn = platformSignInFor(signInSettings && resolvePlatformSignIn(signInSettings), socialProviders);
|
|
247
|
+
const saml = resolveSamlProviders(options.sso?.saml ?? [], [...socialProviders, ...platformSignIn ? [platformSignIn.provider] : []]);
|
|
238
248
|
const origins = resolveOrigins(options.baseURL, options.origins ?? platformOrigins());
|
|
239
249
|
const passkeyDomain = resolvePasskeyDomain(options.passkeyDomain ?? platformPasskeyDomain(), [options.baseURL, ...origins]);
|
|
240
250
|
const organizations = options.organizations && resolveOrganizations(options.organizations, definitions, options.baseURL);
|
|
@@ -251,6 +261,7 @@ const createAuth = (options) => {
|
|
|
251
261
|
signUpRole,
|
|
252
262
|
operator,
|
|
253
263
|
platformSignIn,
|
|
264
|
+
saml,
|
|
254
265
|
...organizations && {
|
|
255
266
|
organizations,
|
|
256
267
|
context
|
|
@@ -281,7 +292,12 @@ const createAuth = (options) => {
|
|
|
281
292
|
platformSignIn: platformSignIn && {
|
|
282
293
|
provider: platformSignIn.provider,
|
|
283
294
|
name: platformSignIn.name
|
|
284
|
-
}
|
|
295
|
+
},
|
|
296
|
+
/** The id and name of each SAML provider, for the sign-in page. */
|
|
297
|
+
samlProviders: saml.map(({ id, name }) => ({
|
|
298
|
+
id,
|
|
299
|
+
name
|
|
300
|
+
}))
|
|
285
301
|
});
|
|
286
302
|
};
|
|
287
303
|
//#endregion
|
|
@@ -29,12 +29,13 @@ const readPlatformSignIn = (env) => {
|
|
|
29
29
|
...name && { name }
|
|
30
30
|
};
|
|
31
31
|
};
|
|
32
|
-
|
|
32
|
+
/** What a sign-in provider id may be: lowercase letters, digits or "-", starting with a letter. */
|
|
33
|
+
const providerIdPattern = /^[a-z][a-z0-9-]*$/;
|
|
33
34
|
/** The provider with its defaults filled in; an issuer that is not a URL or a malformed id is an error. */
|
|
34
35
|
const resolvePlatformSignIn = (signIn) => {
|
|
35
36
|
if (!URL.canParse(signIn.issuer)) throw new Error(`The platform sign-in provider's issuer must be a URL, not "${signIn.issuer}"`);
|
|
36
37
|
const provider = signIn.provider ?? "oidc";
|
|
37
|
-
if (!
|
|
38
|
+
if (!providerIdPattern.test(provider)) throw new Error(`Invalid sign-in provider id "${provider}": use lowercase letters, digits or "-"`);
|
|
38
39
|
return {
|
|
39
40
|
...signIn,
|
|
40
41
|
provider,
|
|
@@ -85,4 +86,4 @@ const platformSignInProvider = (signIn) => ({
|
|
|
85
86
|
getUserInfo: async (tokens) => idTokenUser(tokens.idToken, signIn.issuer)
|
|
86
87
|
});
|
|
87
88
|
//#endregion
|
|
88
|
-
export { idTokenUser, platformSignInFor, platformSignInProvider, readPlatformSignIn, resolvePlatformSignIn, signInClientIdVariable, signInClientSecretVariable, signInIssuerVariable, signInNameVariable, signInProviderVariable };
|
|
89
|
+
export { idTokenUser, platformSignInFor, platformSignInProvider, providerIdPattern, readPlatformSignIn, resolvePlatformSignIn, signInClientIdVariable, signInClientSecretVariable, signInIssuerVariable, signInNameVariable, signInProviderVariable };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "@better-auth/sso";
|
|
2
|
+
//#region src/better-auth/saml-sign-in.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* A SAML 2.0 identity provider people of the app sign in with, for example the company's Okta, Entra ID or Google
|
|
5
|
+
* Workspace. Give its admin this app's entity ID and metadata, `{baseURL}/api/auth/sso/saml2/sp/metadata?providerId={id}`,
|
|
6
|
+
* and the ACS URL `{origin}/api/auth/sso/saml2/sp/acs/{id}` of each origin the app serves.
|
|
7
|
+
*/
|
|
8
|
+
export type SamlProvider = {
|
|
9
|
+
/** The id: the button's `?sign-in=` link, the provider's paths and the namespace of its accounts. Lowercase letters, digits or "-". */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Shown on the sign-in page as "Continue with {name}". Default `SSO`. */
|
|
12
|
+
name?: string;
|
|
13
|
+
/**
|
|
14
|
+
* The email domains of its people, for example `['example.com']`; subdomains count. Only an address there signs up,
|
|
15
|
+
* or links to the account that has it.
|
|
16
|
+
*/
|
|
17
|
+
domains: string[];
|
|
18
|
+
/** The provider's metadata XML, or its entity ID, sign-in URL and signing certificate(s) (PEM; several while one rotates). */
|
|
19
|
+
idp: {
|
|
20
|
+
metadata: string;
|
|
21
|
+
} | {
|
|
22
|
+
entityId: string;
|
|
23
|
+
ssoUrl: string;
|
|
24
|
+
certificate: string | string[];
|
|
25
|
+
};
|
|
26
|
+
/** The assertion's attribute names, when they are not `email`, `givenName`, `surname` and `displayName`. */
|
|
27
|
+
attributes?: {
|
|
28
|
+
email?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
firstName?: string;
|
|
31
|
+
lastName?: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export type SsoOptions = {
|
|
35
|
+
saml: SamlProvider[];
|
|
36
|
+
};
|
|
37
|
+
//#endregion
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { authBasePath } from "./auth-base-path.js";
|
|
2
|
+
import { providerIdPattern } from "./platform-sign-in.js";
|
|
3
|
+
import { APIError, createAuthMiddleware } from "better-auth/api";
|
|
4
|
+
import { sso } from "@better-auth/sso";
|
|
5
|
+
//#region src/better-auth/saml-sign-in.ts
|
|
6
|
+
const domainPattern = /^[a-z0-9-]+(\.[a-z0-9-]+)*$/;
|
|
7
|
+
const checkIdp = (id, idp) => {
|
|
8
|
+
if ("metadata" in idp && typeof idp.metadata === "string" && idp.metadata.trim()) return;
|
|
9
|
+
const manual = idp;
|
|
10
|
+
if (!manual.entityId || !manual.ssoUrl || !manual.certificate || manual.certificate.length === 0) throw new Error(`SAML provider "${id}": give idp.metadata, or all of idp.entityId, idp.ssoUrl and idp.certificate`);
|
|
11
|
+
if (!URL.canParse(manual.ssoUrl)) throw new Error(`SAML provider "${id}": idp.ssoUrl must be a URL, not "${manual.ssoUrl}"`);
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The providers with their defaults filled in: the name `SSO`, and the domains trimmed and lowercased. A malformed id,
|
|
15
|
+
* an id used twice or by another sign-in provider (`takenIds`), no domains or a domain that is not a host name, and IdP
|
|
16
|
+
* settings that are missing or not a URL are errors.
|
|
17
|
+
*/
|
|
18
|
+
const resolveSamlProviders = (saml, takenIds) => {
|
|
19
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20
|
+
return saml.map((provider) => {
|
|
21
|
+
const { id } = provider;
|
|
22
|
+
if (!providerIdPattern.test(id)) throw new Error(`Invalid SAML provider id "${id}": use lowercase letters, digits or "-"`);
|
|
23
|
+
if (seen.has(id)) throw new Error(`SAML provider id "${id}" is used twice`);
|
|
24
|
+
if (takenIds.includes(id)) throw new Error(`SAML provider id "${id}" is already the id of another sign-in provider`);
|
|
25
|
+
seen.add(id);
|
|
26
|
+
const domains = (provider.domains ?? []).map((domain) => domain.trim().toLowerCase());
|
|
27
|
+
if (domains.length === 0) throw new Error(`SAML provider "${id}": give the email domains of its people in domains`);
|
|
28
|
+
const invalid = domains.find((domain) => !domainPattern.test(domain));
|
|
29
|
+
if (invalid !== void 0) throw new Error(`SAML provider "${id}": domains must be host names such as example.com, not "${invalid}"`);
|
|
30
|
+
checkIdp(id, provider.idp);
|
|
31
|
+
return {
|
|
32
|
+
...provider,
|
|
33
|
+
name: provider.name ?? "SSO",
|
|
34
|
+
domains
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
/** The app's SP entity ID for a provider: its metadata URL on `baseURL`, the same on every origin. */
|
|
39
|
+
const samlEntityId = (baseURL, id) => `${baseURL.replace(/\/$/, "")}${authBasePath}/sso/saml2/sp/metadata?providerId=${encodeURIComponent(id)}`;
|
|
40
|
+
/** The provider as one of the SSO plugin's configured providers (`defaultSSO`). */
|
|
41
|
+
const defaultSsoEntry = (provider, baseURL) => {
|
|
42
|
+
const { idp, attributes = {} } = provider;
|
|
43
|
+
const mapping = Object.fromEntries(Object.entries(attributes).filter(([, name]) => name !== void 0));
|
|
44
|
+
return {
|
|
45
|
+
providerId: provider.id,
|
|
46
|
+
domain: provider.domains.join(","),
|
|
47
|
+
samlConfig: {
|
|
48
|
+
issuer: samlEntityId(baseURL, provider.id),
|
|
49
|
+
entryPoint: "ssoUrl" in idp ? idp.ssoUrl : "",
|
|
50
|
+
idpMetadata: "metadata" in idp ? { metadata: idp.metadata } : {
|
|
51
|
+
entityID: idp.entityId,
|
|
52
|
+
cert: idp.certificate
|
|
53
|
+
},
|
|
54
|
+
...Object.keys(mapping).length > 0 && { mapping }
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Better Auth's SSO plugin with the configured providers. Their domains are trusted, so an address there links to the
|
|
60
|
+
* account that has it; organizations are not assigned by provider, and nobody registers providers.
|
|
61
|
+
*/
|
|
62
|
+
const samlPlugin = (providers, baseURL) => {
|
|
63
|
+
const { schema: _schema, ...plugin } = sso({
|
|
64
|
+
defaultSSO: providers.map((provider) => defaultSsoEntry(provider, baseURL)),
|
|
65
|
+
domainVerification: { enabled: true },
|
|
66
|
+
organizationProvisioning: { disabled: true },
|
|
67
|
+
providersLimit: 0
|
|
68
|
+
});
|
|
69
|
+
return plugin;
|
|
70
|
+
};
|
|
71
|
+
/** The SSO plugin's endpoints for providers kept in its table, and its OIDC callback, which Protobase does not serve. */
|
|
72
|
+
const samlDisabledPaths = [
|
|
73
|
+
"/sso/register",
|
|
74
|
+
"/sso/providers",
|
|
75
|
+
"/sso/get-provider",
|
|
76
|
+
"/sso/update-provider",
|
|
77
|
+
"/sso/delete-provider",
|
|
78
|
+
"/sso/request-domain-verification",
|
|
79
|
+
"/sso/verify-domain",
|
|
80
|
+
"/sso/callback"
|
|
81
|
+
];
|
|
82
|
+
const providerPaths = [
|
|
83
|
+
"/sso/saml2/sp/acs/:providerId",
|
|
84
|
+
"/sso/saml2/sp/slo/:providerId",
|
|
85
|
+
"/sso/saml2/logout/:providerId"
|
|
86
|
+
];
|
|
87
|
+
const signInPath = "/sign-in/sso";
|
|
88
|
+
const metadataPath = "/sso/saml2/sp/metadata";
|
|
89
|
+
const oidcCallbackPath = "/sso/callback/:providerId";
|
|
90
|
+
/** Whether `path`, as `ctx.path` names it, is one of the SSO plugin's endpoints that name a provider. */
|
|
91
|
+
const isSamlPath = (path) => path !== void 0 && [
|
|
92
|
+
signInPath,
|
|
93
|
+
metadataPath,
|
|
94
|
+
oidcCallbackPath,
|
|
95
|
+
...providerPaths
|
|
96
|
+
].includes(path);
|
|
97
|
+
/**
|
|
98
|
+
* Why a request to the SSO plugin is refused, or `undefined` when it names a configured provider: a request that names
|
|
99
|
+
* none, or another one, would make the plugin look in its provider table. Sign-in names the provider by `providerId`
|
|
100
|
+
* only, not by an address, domain or organization; the OIDC callback is always refused.
|
|
101
|
+
*/
|
|
102
|
+
const samlRefusal = ({ path, body, query, params }, ids) => {
|
|
103
|
+
if (!isSamlPath(path)) return void 0;
|
|
104
|
+
if (path === oidcCallbackPath) return "No OpenID Connect provider is configured";
|
|
105
|
+
const named = path === signInPath ? body?.providerId : path === metadataPath ? query?.providerId : params?.providerId;
|
|
106
|
+
if (typeof named !== "string" || !named) return "Name the SAML provider by its providerId";
|
|
107
|
+
return ids.includes(named) ? void 0 : `No SAML provider ${named}`;
|
|
108
|
+
};
|
|
109
|
+
/** Keeps every request to the SSO plugin to the configured providers; see `samlRefusal`. */
|
|
110
|
+
const samlGuardPlugin = (ids) => ({
|
|
111
|
+
id: "protobase-saml-guard",
|
|
112
|
+
hooks: { before: [{
|
|
113
|
+
matcher: (context) => isSamlPath(context.path),
|
|
114
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
115
|
+
const refusal = samlRefusal({
|
|
116
|
+
path: ctx.path,
|
|
117
|
+
body: ctx.body,
|
|
118
|
+
query: ctx.query,
|
|
119
|
+
params: ctx.params
|
|
120
|
+
}, ids);
|
|
121
|
+
if (refusal) throw new APIError("NOT_FOUND", { message: refusal });
|
|
122
|
+
})
|
|
123
|
+
}] }
|
|
124
|
+
});
|
|
125
|
+
/** Whether the address is at one of the domains, or a subdomain of one. */
|
|
126
|
+
const emailInDomains = (email, domains) => {
|
|
127
|
+
const at = email.lastIndexOf("@");
|
|
128
|
+
if (at < 0) return false;
|
|
129
|
+
const domain = email.slice(at + 1).toLowerCase();
|
|
130
|
+
return domains.some((allowed) => domain === allowed || domain.endsWith(`.${allowed}`));
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
export { defaultSsoEntry, emailInDomains, isSamlPath, resolveSamlProviders, samlDisabledPaths, samlEntityId, samlGuardPlugin, samlPlugin, samlRefusal };
|
|
@@ -10,7 +10,7 @@ import { Hono } from "hono";
|
|
|
10
10
|
* `signInMethods`, the ways to sign in the sign-in policy leaves on (`password`, `emailCode`, `passkey`), so it offers
|
|
11
11
|
* only those; `passwordReset` when reset mail can be sent and passwords are on, so it offers "Forgot password?" only
|
|
12
12
|
* then; `signUp` when the app has open sign-up, so it offers "Create an account"; the ids of the `socialProviders` people can sign in with, so it offers "Continue with GitHub" when `github` is
|
|
13
|
-
* one; `platformSignIn`, the id and name of the platform's provider when it is one of them; `staffSignIn`, the
|
|
13
|
+
* one; `platformSignIn`, the id and name of the platform's provider when it is one of them; `samlProviders`, the id and name of each SAML provider, when the app has any; `staffSignIn`, the
|
|
14
14
|
* operator provider's name, when staff can sign in as people; `passkeyDomains`, when people have passkeys for more than
|
|
15
15
|
* one of the app's hosts, the RP IDs a passkey sign-in can ask for, the passkey domain first; with organizations, who may create them and the roles
|
|
16
16
|
* with their labels, `membership` for those a member can hold, and the roles each one's holders may also give; and
|
|
@@ -39,6 +39,7 @@ const statusRoute = (auth, { anonymous = false, plugins = [] } = {}) => {
|
|
|
39
39
|
...auth.signUp && { signUp: true },
|
|
40
40
|
socialProviders,
|
|
41
41
|
...platformSignIn && { platformSignIn },
|
|
42
|
+
...auth.samlProviders.length > 0 && { samlProviders: auth.samlProviders },
|
|
42
43
|
...staffSignIn && { staffSignIn },
|
|
43
44
|
...passkeyDomains.length > 1 && { passkeyDomains },
|
|
44
45
|
...anonymous && { anonymous },
|
package/dist/check-views.js
CHANGED
|
@@ -6,7 +6,9 @@ import { chartErrors } from "./view-chart.js";
|
|
|
6
6
|
import { mapDisplayErrors, mapErrors } from "./view-map.js";
|
|
7
7
|
import { relatedColumnErrors } from "./view-related-column.js";
|
|
8
8
|
import { editableErrors } from "./view-editable.js";
|
|
9
|
+
import { conditionProblems } from "@protobase/layout";
|
|
9
10
|
import { actionInputErrors, addressErrors, addressPlaceholders, boardErrors, calendarErrors, chartFields, displayFields, fieldRefsIn, hrefField, hrefPlaceholders, linkActionError, promptError, promptFields } from "@protobase/schema/internal";
|
|
11
|
+
import { isRecordCondition } from "@protobase/schema";
|
|
10
12
|
//#region src/check-views.ts
|
|
11
13
|
const linkFields = (run) => run?.kind !== "link" ? [] : "to" in run ? addressPlaceholders(run.to) : hrefPlaceholders(run.href);
|
|
12
14
|
const actionFields = (run) => {
|
|
@@ -123,6 +125,12 @@ const galleryErrors = (item, resource, model, models) => {
|
|
|
123
125
|
const image = item.image === void 0 ? void 0 : model.fields[item.image];
|
|
124
126
|
return [...image && image.type !== "file" ? [`"${item.image}" is not a file field`] : [], ...item.photos ? cardPhotosErrors(item.photos, resource, models).map((error) => `photos: ${error}`) : []];
|
|
125
127
|
};
|
|
128
|
+
const conditionErrors = (view, models) => view.layout.flatMap((item) => {
|
|
129
|
+
if (item.kind !== "section" && item.kind !== "sidebar" && item.kind !== "related") return [];
|
|
130
|
+
if (!isRecordCondition(item.when)) return [];
|
|
131
|
+
const place = item.kind === "sidebar" ? "sidebar" : `${item.kind} "${item.title}"`;
|
|
132
|
+
return conditionProblems(item.when, view.resource, models).map((error) => `${place}: ${error}`);
|
|
133
|
+
});
|
|
126
134
|
const listOfErrors = (model, name, models) => {
|
|
127
135
|
const target = models[model.fields[name]?.relation?.resource ?? ""];
|
|
128
136
|
if (!target) return [`"${name}" points at no resource of the app`];
|
|
@@ -133,7 +141,7 @@ const listOfErrors = (model, name, models) => {
|
|
|
133
141
|
* field the resource does not have, labels or colours values its enum does not have, shows a field with a field type of
|
|
134
142
|
* `fieldTypes` (each with the storage types it shows) that does not show it,
|
|
135
143
|
* edits a list column in its cell that is not a field with an editor of one line the API may write,
|
|
136
|
-
* has a related section or related list column, a cards display's or gallery's photos or a cards display's favorite button, a `listOf`, a "Save search" button or a
|
|
144
|
+
* has a section, related section or sidebar whose `when` condition names what the app does not have, a related section or related list column, a cards display's or gallery's photos or a cards display's favorite button, a `listOf`, a "Save search" button or a
|
|
137
145
|
* `searchesOf` that do not fit the resources, cards
|
|
138
146
|
* displays of one list that name different photos, a chart per value that cannot group or add up its fields, a map that cannot show the
|
|
139
147
|
* record's position or a map display that cannot find the records in its view, a board whose columns are no filterable enum, a calendar whose dates are no filterable date or timestamp fields, a link to an address the app does not have, an AI action that cannot write its field, action inputs that do not fit, or a create page that is not one of `pages`.
|
|
@@ -168,6 +176,7 @@ const checkViews = (registry, views, pages = [], fieldTypes = {}) => {
|
|
|
168
176
|
...listEditErrors(view, model),
|
|
169
177
|
...widgetErrors(view, model, fieldTypes),
|
|
170
178
|
...related,
|
|
179
|
+
...conditionErrors(view, models),
|
|
171
180
|
...photos,
|
|
172
181
|
...favorites,
|
|
173
182
|
...listOf,
|
package/dist/create-admin.js
CHANGED
|
@@ -11,7 +11,7 @@ import { consoleAuditQueue } from "./audit/console-queue.js";
|
|
|
11
11
|
import { webauthnPath } from "./better-auth/origins.js";
|
|
12
12
|
import { apiCors, readCorsOrigins } from "./api-cors.js";
|
|
13
13
|
import { readAppName } from "./app-name.js";
|
|
14
|
-
import { authBasePath } from "./better-auth/
|
|
14
|
+
import { authBasePath } from "./better-auth/auth-base-path.js";
|
|
15
15
|
import { organizationRoutes } from "./better-auth/organizations/admin-routes.js";
|
|
16
16
|
import { statusRoute } from "./better-auth/status-route.js";
|
|
17
17
|
import { changesSchema } from "./changes/events-options.js";
|
package/dist/view-access.js
CHANGED
|
@@ -3,10 +3,12 @@ import { cardPhotosErrors } from "./view-card-photos.js";
|
|
|
3
3
|
import { cardFavoriteErrors } from "./view-card-favorite.js";
|
|
4
4
|
import { searchesOfFor } from "./view-saved-search.js";
|
|
5
5
|
import { relatedColumnErrors } from "./view-related-column.js";
|
|
6
|
-
import { resolveFieldPath } from "@protobase/layout";
|
|
6
|
+
import { conditionReadable, resolveFieldPath } from "@protobase/layout";
|
|
7
7
|
import { addressErrors, chartFields, hrefPlaceholders, navRecentErrors, promptFields, pruneByShape, shapes, tableDisplay } from "@protobase/schema/internal";
|
|
8
|
+
import { isRecordCondition } from "@protobase/schema";
|
|
8
9
|
//#region src/view-access.ts
|
|
9
10
|
const pruneRelated = (item, resource, models) => {
|
|
11
|
+
if (isRecordCondition(item.when) && !conditionReadable(item.when, resource, models)) return void 0;
|
|
10
12
|
const { columns, open, ...rest } = item;
|
|
11
13
|
const visible = models[item.resource] ? columns?.filter((column) => resolveFieldPath(models, item.resource, column).ok) : void 0;
|
|
12
14
|
const narrowed = visible && visible.length > 0 ? {
|
|
@@ -22,6 +24,7 @@ const pruneRelated = (item, resource, models) => {
|
|
|
22
24
|
} : narrowed;
|
|
23
25
|
return relatedErrors(opened, resource, models).length === 0 ? opened : void 0;
|
|
24
26
|
};
|
|
27
|
+
const recordWhenShape = (resource, models) => shapes.map((when) => isRecordCondition(when) && !conditionReadable(when, resource, models) ? void 0 : when);
|
|
25
28
|
const pruneGallery = (item, resource, model, models) => {
|
|
26
29
|
const image = item.image !== void 0 && Object.hasOwn(model.fields, item.image) ? item.image : void 0;
|
|
27
30
|
const photos = item.photos && cardPhotosErrors(item.photos, resource, models).length === 0 ? item.photos : void 0;
|
|
@@ -162,14 +165,14 @@ const layoutShape = (resource, model, models) => shapes.oneOf({
|
|
|
162
165
|
help: shapes.data,
|
|
163
166
|
helpWhen: shapes.data,
|
|
164
167
|
presentation: shapes.data,
|
|
165
|
-
when:
|
|
168
|
+
when: recordWhenShape(resource, models),
|
|
166
169
|
fields: shapes.list(shapes.field, { nonEmpty: true })
|
|
167
|
-
}, { requires: ["fields"] }),
|
|
170
|
+
}, { requires: ["fields", "when"] }),
|
|
168
171
|
sidebar: shapes.object({
|
|
169
172
|
kind: shapes.data,
|
|
170
173
|
fields: shapes.list(shapes.field, { nonEmpty: true }),
|
|
171
|
-
when:
|
|
172
|
-
}, { requires: ["fields"] }),
|
|
174
|
+
when: recordWhenShape(resource, models)
|
|
175
|
+
}, { requires: ["fields", "when"] }),
|
|
173
176
|
related: shapes.map((item) => pruneRelated(item, resource, models)),
|
|
174
177
|
custom: shapes.object({
|
|
175
178
|
kind: shapes.data,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.11",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -30,9 +30,10 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@better-auth/passkey": "^1.7.7",
|
|
33
|
-
"@
|
|
34
|
-
"@protobase/
|
|
35
|
-
"@protobase/
|
|
33
|
+
"@better-auth/sso": "^1.7.7",
|
|
34
|
+
"@protobase/layout": "0.21.11",
|
|
35
|
+
"@protobase/query": "0.21.11",
|
|
36
|
+
"@protobase/schema": "0.21.11",
|
|
36
37
|
"@react-email/render": "^2.1.0",
|
|
37
38
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
38
39
|
"@sentry/core": "^11.7.0",
|
|
@@ -60,5 +61,8 @@
|
|
|
60
61
|
"sharp": {
|
|
61
62
|
"optional": true
|
|
62
63
|
}
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"samlify": "^2.13.1"
|
|
63
67
|
}
|
|
64
68
|
}
|