@paneljs/paneljs 0.1.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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/adapter.d.ts +24 -0
  4. package/dist/adapter.js +2 -0
  5. package/dist/adapter.js.map +1 -0
  6. package/dist/audit.d.ts +5 -0
  7. package/dist/audit.js +11 -0
  8. package/dist/audit.js.map +1 -0
  9. package/dist/authMode.d.ts +2 -0
  10. package/dist/authMode.js +2 -0
  11. package/dist/authMode.js.map +1 -0
  12. package/dist/defaultActions.d.ts +5 -0
  13. package/dist/defaultActions.js +6 -0
  14. package/dist/defaultActions.js.map +1 -0
  15. package/dist/errors.d.ts +18 -0
  16. package/dist/errors.js +36 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/ids.d.ts +3 -0
  19. package/dist/ids.js +14 -0
  20. package/dist/ids.js.map +1 -0
  21. package/dist/index.d.ts +27 -0
  22. package/dist/index.js +40 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/listQuery.d.ts +19 -0
  25. package/dist/listQuery.js +106 -0
  26. package/dist/listQuery.js.map +1 -0
  27. package/dist/passwords.d.ts +4 -0
  28. package/dist/passwords.js +57 -0
  29. package/dist/passwords.js.map +1 -0
  30. package/dist/permissions.d.ts +15 -0
  31. package/dist/permissions.js +36 -0
  32. package/dist/permissions.js.map +1 -0
  33. package/dist/recordSelection.d.ts +13 -0
  34. package/dist/recordSelection.js +65 -0
  35. package/dist/recordSelection.js.map +1 -0
  36. package/dist/registry.d.ts +156 -0
  37. package/dist/registry.js +388 -0
  38. package/dist/registry.js.map +1 -0
  39. package/dist/schema.d.ts +8 -0
  40. package/dist/schema.js +59 -0
  41. package/dist/schema.js.map +1 -0
  42. package/dist/scope.d.ts +31 -0
  43. package/dist/scope.js +86 -0
  44. package/dist/scope.js.map +1 -0
  45. package/dist/types.d.ts +189 -0
  46. package/dist/types.js +2 -0
  47. package/dist/types.js.map +1 -0
  48. package/dist/uiPath.d.ts +2 -0
  49. package/dist/uiPath.js +7 -0
  50. package/dist/uiPath.js.map +1 -0
  51. package/dist/validation.d.ts +29 -0
  52. package/dist/validation.js +147 -0
  53. package/dist/validation.js.map +1 -0
  54. package/package.json +54 -0
  55. package/ui/dist/assets/index-ChtweaBk.js +11 -0
  56. package/ui/dist/assets/index-eXfQCDXx.css +1 -0
  57. package/ui/dist/fonts/dm-mono-400.woff2 +0 -0
  58. package/ui/dist/fonts/dm-mono-500.woff2 +0 -0
  59. package/ui/dist/fonts/dm-sans-400.woff2 +0 -0
  60. package/ui/dist/fonts/dm-sans-500.woff2 +0 -0
  61. package/ui/dist/fonts/dm-sans-600.woff2 +0 -0
  62. package/ui/dist/fonts/dm-sans-700.woff2 +0 -0
  63. package/ui/dist/index.html +14 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Divinesta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # paneljs
2
+
3
+ Schema-driven admin core. Register models and serve a guarded operations UI.
4
+
5
+ Install with the adapters for your stack:
6
+
7
+ ```bash
8
+ npm install paneljs @paneljs/express @paneljs/prisma
9
+ ```
10
+
11
+ ```ts
12
+ import { createAdmin } from "paneljs";
13
+ import { prismaAdapter } from "@paneljs/prisma";
14
+ import { mount } from "@paneljs/express";
15
+
16
+ const admin = createAdmin({
17
+ adapter: prismaAdapter({ prisma }),
18
+ auth: { getCurrentUser },
19
+ });
20
+
21
+ admin.register("User");
22
+ await mount(app, admin);
23
+ ```
24
+
25
+ See the [repository](https://github.com/divinesta/paneljs) for docs and the example app.
@@ -0,0 +1,24 @@
1
+ import type { AdminModelMeta } from "./types.js";
2
+ /** The subset of CRUD a host ORM must implement for one model. */
3
+ export interface ModelResource {
4
+ findMany(args: Record<string, unknown>): Promise<Record<string, unknown>[]>;
5
+ findFirst(args: Record<string, unknown>): Promise<Record<string, unknown> | null>;
6
+ count(args: Record<string, unknown>): Promise<number>;
7
+ create(args: Record<string, unknown>): Promise<Record<string, unknown>>;
8
+ updateMany(args: Record<string, unknown>): Promise<{
9
+ count: number;
10
+ }>;
11
+ deleteMany(args: Record<string, unknown>): Promise<{
12
+ count: number;
13
+ }>;
14
+ }
15
+ /**
16
+ * Host-supplied data layer. Core never imports an ORM.
17
+ * Prisma, TypeORM, and Drizzle each implement this.
18
+ */
19
+ export interface DataAdapter {
20
+ /** Opaque client handed to custom action handlers. */
21
+ readonly client: unknown;
22
+ introspect(): Promise<Map<string, AdminModelMeta>>;
23
+ resource(meta: AdminModelMeta): ModelResource;
24
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.js","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ import type { AdminAuditEvent, AdminUser, AuditConfig } from "./types.js";
2
+ type AuditEventInput = Omit<AdminAuditEvent, "actor" | "timestamp">;
3
+ /** Write a mutation event when the host configured an audit destination. */
4
+ export declare function writeAuditEvent(audit: AuditConfig | undefined, adminUser: AdminUser, event: AuditEventInput): Promise<void>;
5
+ export {};
package/dist/audit.js ADDED
@@ -0,0 +1,11 @@
1
+ /** Write a mutation event when the host configured an audit destination. */
2
+ export async function writeAuditEvent(audit, adminUser, event) {
3
+ if (!audit)
4
+ return;
5
+ await audit.write({
6
+ ...event,
7
+ actor: { id: adminUser.id, email: adminUser.email, role: adminUser.role },
8
+ timestamp: new Date(),
9
+ });
10
+ }
11
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAIA,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAA8B,EAAE,SAAoB,EAAE,KAAsB;IAC/G,IAAI,CAAC,KAAK;QAAE,OAAO;IAEnB,MAAM,KAAK,CAAC,KAAK,CAAC;QACf,GAAG,KAAK;QACR,KAAK,EAAE,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE;QACzE,SAAS,EAAE,IAAI,IAAI,EAAE;KACvB,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { AuthConfig, BuiltInAuthConfig } from "./types.js";
2
+ export declare const isBuiltInAuth: (auth: AuthConfig) => auth is BuiltInAuthConfig;
@@ -0,0 +1,2 @@
1
+ export const isBuiltInAuth = (auth) => auth.mode === "built-in";
2
+ //# sourceMappingURL=authMode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authMode.js","sourceRoot":"","sources":["../src/authMode.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,IAAgB,EAA6B,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC"}
@@ -0,0 +1,5 @@
1
+ /** Built-in list actions supplied by the admin rather than a model config. */
2
+ export declare const DELETE_SELECTED_ACTION: {
3
+ readonly name: "delete_selected";
4
+ readonly label: "Delete selected";
5
+ };
@@ -0,0 +1,6 @@
1
+ /** Built-in list actions supplied by the admin rather than a model config. */
2
+ export const DELETE_SELECTED_ACTION = {
3
+ name: "delete_selected",
4
+ label: "Delete selected",
5
+ };
6
+ //# sourceMappingURL=defaultActions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaultActions.js","sourceRoot":"","sources":["../src/defaultActions.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,iBAAiB;IACvB,KAAK,EAAE,iBAAiB;CACjB,CAAC"}
@@ -0,0 +1,18 @@
1
+ /** A deliberate, safe error that may be returned to an admin API caller. */
2
+ export declare class AdminApiError extends Error {
3
+ readonly status: number;
4
+ readonly code: string;
5
+ constructor(status: number, code: string, message: string);
6
+ }
7
+ export declare class AuthenticationError extends AdminApiError {
8
+ constructor();
9
+ }
10
+ export declare class PermissionDeniedError extends AdminApiError {
11
+ constructor();
12
+ }
13
+ export declare class ModelNotFoundError extends AdminApiError {
14
+ constructor();
15
+ }
16
+ export declare class RecordNotFoundError extends AdminApiError {
17
+ constructor();
18
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,36 @@
1
+ /** A deliberate, safe error that may be returned to an admin API caller. */
2
+ export class AdminApiError extends Error {
3
+ status;
4
+ code;
5
+ constructor(status, code, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ this.name = "AdminApiError";
10
+ }
11
+ }
12
+ export class AuthenticationError extends AdminApiError {
13
+ constructor() {
14
+ super(401, "AUTHENTICATION_REQUIRED", "Authentication required");
15
+ this.name = "AuthenticationError";
16
+ }
17
+ }
18
+ export class PermissionDeniedError extends AdminApiError {
19
+ constructor() {
20
+ super(403, "PERMISSION_DENIED", "Permission denied");
21
+ this.name = "PermissionDeniedError";
22
+ }
23
+ }
24
+ export class ModelNotFoundError extends AdminApiError {
25
+ constructor() {
26
+ super(404, "MODEL_NOT_FOUND", "Model not found");
27
+ this.name = "ModelNotFoundError";
28
+ }
29
+ }
30
+ export class RecordNotFoundError extends AdminApiError {
31
+ constructor() {
32
+ super(404, "RECORD_NOT_FOUND", "Record not found");
33
+ this.name = "RecordNotFoundError";
34
+ }
35
+ }
36
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,OAAO,aAAc,SAAQ,KAAK;IAEzB;IACA;IAFZ,YACY,MAAc,EACd,IAAY,EACrB,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAJN,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAIrB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC/B,CAAC;CACH;AAED,MAAM,OAAO,mBAAoB,SAAQ,aAAa;IACnD;QACG,KAAK,CAAC,GAAG,EAAE,yBAAyB,EAAE,yBAAyB,CAAC,CAAC;QACjE,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACrC,CAAC;CACH;AAED,MAAM,OAAO,qBAAsB,SAAQ,aAAa;IACrD;QACG,KAAK,CAAC,GAAG,EAAE,mBAAmB,EAAE,mBAAmB,CAAC,CAAC;QACrD,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACvC,CAAC;CACH;AAED,MAAM,OAAO,kBAAmB,SAAQ,aAAa;IAClD;QACG,KAAK,CAAC,GAAG,EAAE,iBAAiB,EAAE,iBAAiB,CAAC,CAAC;QACjD,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACpC,CAAC;CACH;AAED,MAAM,OAAO,mBAAoB,SAAQ,aAAa;IACnD;QACG,KAAK,CAAC,GAAG,EAAE,kBAAkB,EAAE,kBAAkB,CAAC,CAAC;QACnD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACrC,CAAC;CACH"}
package/dist/ids.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { AdminModelMeta } from "./types.js";
2
+ /** Parse a path or action id into the model's primary-key type. */
3
+ export declare function parseRecordId(meta: AdminModelMeta, rawId: string): string | number;
package/dist/ids.js ADDED
@@ -0,0 +1,14 @@
1
+ import { RequestValidationError } from "./validation.js";
2
+ /** Parse a path or action id into the model's primary-key type. */
3
+ export function parseRecordId(meta, rawId) {
4
+ const idField = meta.fields.find((field) => field.name === meta.idField);
5
+ if (idField?.type !== "number")
6
+ return rawId;
7
+ if (!/^-?(?:0|[1-9]\d*)$/.test(rawId))
8
+ throw new RequestValidationError(`Record ID for "${meta.name}" must be an integer.`);
9
+ const id = Number(rawId);
10
+ if (!Number.isSafeInteger(id))
11
+ throw new RequestValidationError(`Record ID for "${meta.name}" must be a safe integer.`);
12
+ return id;
13
+ }
14
+ //# sourceMappingURL=ids.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ids.js","sourceRoot":"","sources":["../src/ids.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,mEAAmE;AACnE,MAAM,UAAU,aAAa,CAAC,IAAoB,EAAE,KAAa;IAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,OAAO,CAAC,CAAC;IACzE,IAAI,OAAO,EAAE,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE7C,IAAI,CAAC,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,sBAAsB,CAAC,kBAAkB,IAAI,CAAC,IAAI,uBAAuB,CAAC,CAAC;IAC5H,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,EAAE,CAAC;QAAE,MAAM,IAAI,sBAAsB,CAAC,kBAAkB,IAAI,CAAC,IAAI,2BAA2B,CAAC,CAAC;IACxH,OAAO,EAAE,CAAC;AACb,CAAC"}
@@ -0,0 +1,27 @@
1
+ import type { AdminConfig, ModelConfig } from "./types.js";
2
+ import { AdminRegistry } from "./registry.js";
3
+ export interface Admin {
4
+ register(modelName: string, modelConfig?: ModelConfig): Admin;
5
+ initialize(): Promise<void>;
6
+ readonly registry: AdminRegistry;
7
+ readonly config: AdminConfig;
8
+ }
9
+ export declare function createAdmin(config: AdminConfig): Admin;
10
+ export type { AdminConfig, ModelConfig, AdminUser, AuthConfig, BuiltInAuthConfig, ExternalAuthConfig, AdminHttpRequest, AuditConfig, AdminAuditEvent, AdminFieldMeta, AdminModelMeta, AdminFieldType, RelationKind, ModelPermissions, AdminAction, AdminFieldOverride, PaginatedResponse, SchemaResponse, } from "./types.js";
11
+ export type { DataAdapter, ModelResource } from "./adapter.js";
12
+ export type { ResolvedModelConfig, FullRegisteredModel } from "./registry.js";
13
+ export { AdminRegistry } from "./registry.js";
14
+ export { hashAdminPassword, verifyAdminPassword, verifyLoginPassword } from "./passwords.js";
15
+ export { AdminApiError, AuthenticationError, PermissionDeniedError, ModelNotFoundError, RecordNotFoundError } from "./errors.js";
16
+ export { RequestValidationError, isFieldVisible, isFieldWritable, isSensitiveFieldName, validateWritePayload, validateHookPayload, assertRequiredCreateFields } from "./validation.js";
17
+ export { hasModelPermission, hasActionPermission, hasRegisteredActionPermission } from "./permissions.js";
18
+ export type { AdminOperation } from "./permissions.js";
19
+ export { DELETE_SELECTED_ACTION } from "./defaultActions.js";
20
+ export { writeAuditEvent } from "./audit.js";
21
+ export { buildSchemaResponse } from "./schema.js";
22
+ export { parseListQuery, buildListWhere } from "./listQuery.js";
23
+ export { parseRecordId } from "./ids.js";
24
+ export { buildRecordSelect, buildListRecordSelect, assertSelectedRelationsAreVisible } from "./recordSelection.js";
25
+ export { applyCreateScope, assertScopeFieldsUnchanged, buildScopedRecordWhere, collectScopeFieldNames, resolveScope } from "./scope.js";
26
+ export { getAdminUiDist } from "./uiPath.js";
27
+ export { isBuiltInAuth } from "./authMode.js";
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ import { AdminRegistry } from "./registry.js";
2
+ export function createAdmin(config) {
3
+ const registry = new AdminRegistry();
4
+ let initialized = false;
5
+ const admin = {
6
+ register(modelName, modelConfig = {}) {
7
+ registry.register(modelName, modelConfig);
8
+ return admin;
9
+ },
10
+ async initialize() {
11
+ if (initialized)
12
+ return;
13
+ const models = await config.adapter.introspect();
14
+ await registry.initialize(models);
15
+ initialized = true;
16
+ },
17
+ get registry() {
18
+ return registry;
19
+ },
20
+ get config() {
21
+ return config;
22
+ },
23
+ };
24
+ return admin;
25
+ }
26
+ export { AdminRegistry } from "./registry.js";
27
+ export { hashAdminPassword, verifyAdminPassword, verifyLoginPassword } from "./passwords.js";
28
+ export { AdminApiError, AuthenticationError, PermissionDeniedError, ModelNotFoundError, RecordNotFoundError } from "./errors.js";
29
+ export { RequestValidationError, isFieldVisible, isFieldWritable, isSensitiveFieldName, validateWritePayload, validateHookPayload, assertRequiredCreateFields } from "./validation.js";
30
+ export { hasModelPermission, hasActionPermission, hasRegisteredActionPermission } from "./permissions.js";
31
+ export { DELETE_SELECTED_ACTION } from "./defaultActions.js";
32
+ export { writeAuditEvent } from "./audit.js";
33
+ export { buildSchemaResponse } from "./schema.js";
34
+ export { parseListQuery, buildListWhere } from "./listQuery.js";
35
+ export { parseRecordId } from "./ids.js";
36
+ export { buildRecordSelect, buildListRecordSelect, assertSelectedRelationsAreVisible } from "./recordSelection.js";
37
+ export { applyCreateScope, assertScopeFieldsUnchanged, buildScopedRecordWhere, collectScopeFieldNames, resolveScope } from "./scope.js";
38
+ export { getAdminUiDist } from "./uiPath.js";
39
+ export { isBuiltInAuth } from "./authMode.js";
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAS9C,MAAM,UAAU,WAAW,CAAC,MAAmB;IAC5C,MAAM,QAAQ,GAAG,IAAI,aAAa,EAAE,CAAC;IACrC,IAAI,WAAW,GAAG,KAAK,CAAC;IAExB,MAAM,KAAK,GAAU;QAClB,QAAQ,CAAC,SAAiB,EAAE,cAA2B,EAAE;YACtD,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;YAC1C,OAAO,KAAK,CAAC;QAChB,CAAC;QACD,KAAK,CAAC,UAAU;YACb,IAAI,WAAW;gBAAE,OAAO;YACxB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;YACjD,MAAM,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAClC,WAAW,GAAG,IAAI,CAAC;QACtB,CAAC;QACD,IAAI,QAAQ;YACT,OAAO,QAAQ,CAAC;QACnB,CAAC;QACD,IAAI,MAAM;YACP,OAAO,MAAM,CAAC;QACjB,CAAC;KACH,CAAC;IAEF,OAAO,KAAK,CAAC;AAChB,CAAC;AAwBD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC7F,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACjI,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,eAAe,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AACvL,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,6BAA6B,EAAE,MAAM,kBAAkB,CAAC;AAE1G,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,iCAAiC,EAAE,MAAM,sBAAsB,CAAC;AACnH,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACxI,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { FullRegisteredModel } from "./registry.js";
2
+ import type { AdminModelMeta } from "./types.js";
3
+ export type QueryValue = string | string[] | undefined;
4
+ export type QueryMap = Record<string, QueryValue>;
5
+ export declare function buildListWhere(scope: Record<string, unknown>, filters: Record<string, unknown>, search: Record<string, unknown> | undefined): Record<string, unknown>;
6
+ export declare function parseListQuery(query: QueryMap, meta: AdminModelMeta, model: FullRegisteredModel, databaseProvider?: string): {
7
+ page: number;
8
+ sort: string;
9
+ dir: string;
10
+ filters: Record<string, unknown>;
11
+ search: {
12
+ OR: {
13
+ [x: string]: {
14
+ mode?: string | undefined;
15
+ contains: string;
16
+ };
17
+ }[];
18
+ } | undefined;
19
+ };
@@ -0,0 +1,106 @@
1
+ import { isFieldVisible, RequestValidationError } from "./validation.js";
2
+ function getQueryValue(query, name) {
3
+ const value = query[name];
4
+ if (value === undefined)
5
+ return undefined;
6
+ if (typeof value !== "string")
7
+ throw new RequestValidationError(`Query parameter "${name}" must be a single string value.`);
8
+ return value;
9
+ }
10
+ function parseFilterValue(field, value) {
11
+ switch (field.type) {
12
+ case "string":
13
+ case "bytes":
14
+ return value;
15
+ case "number": {
16
+ const number = Number(value);
17
+ if (!Number.isFinite(number))
18
+ throw new RequestValidationError(`Filter "${field.name}" must be a finite number.`);
19
+ return number;
20
+ }
21
+ case "boolean":
22
+ if (value !== "true" && value !== "false")
23
+ throw new RequestValidationError(`Filter "${field.name}" must be "true" or "false".`);
24
+ return value === "true";
25
+ case "datetime": {
26
+ const date = new Date(value);
27
+ if (Number.isNaN(date.getTime()))
28
+ throw new RequestValidationError(`Filter "${field.name}" must be an ISO date-time value.`);
29
+ return date;
30
+ }
31
+ case "enum":
32
+ if (!field.enumValues?.includes(value))
33
+ throw new RequestValidationError(`Filter "${field.name}" must be a valid ${field.nativeType} value.`);
34
+ return value;
35
+ case "json":
36
+ case "relation":
37
+ throw new RequestValidationError(`Filter "${field.name}" is not supported.`);
38
+ }
39
+ }
40
+ export function buildListWhere(scope, filters, search) {
41
+ const conditions = [scope, filters, search].filter((condition) => condition !== undefined && Object.keys(condition).length > 0);
42
+ if (conditions.length === 0)
43
+ return {};
44
+ if (conditions.length === 1)
45
+ return conditions[0] ?? {};
46
+ return { AND: conditions };
47
+ }
48
+ export function parseListQuery(query, meta, model, databaseProvider) {
49
+ const pageValue = Number(getQueryValue(query, "page") ?? 1);
50
+ if (!Number.isInteger(pageValue) || pageValue < 1)
51
+ throw new RequestValidationError("Query parameter \"page\" must be a positive integer.");
52
+ if (pageValue > 10_000)
53
+ throw new RequestValidationError("Query parameter \"page\" must be 10,000 or fewer.");
54
+ const sort = getQueryValue(query, "sort") ?? model.resolved.defaultSort.field;
55
+ const dir = getQueryValue(query, "dir") ?? model.resolved.defaultSort.direction;
56
+ const sortableFields = new Set(meta.fields.filter((field) => field.type !== "relation" && isFieldVisible(field, model.raw)).map((field) => field.name));
57
+ if (!sortableFields.has(sort))
58
+ throw new RequestValidationError(`Field "${sort}" cannot be used for sorting.`);
59
+ if (dir !== "asc" && dir !== "desc")
60
+ throw new RequestValidationError("Query parameter \"dir\" must be either \"asc\" or \"desc\".");
61
+ const fieldsByName = new Map(meta.fields.map((field) => [field.name, field]));
62
+ const filterableFields = new Set(model.resolved.listFilter.filter((fieldName) => {
63
+ const field = fieldsByName.get(fieldName);
64
+ return field !== undefined && field.type !== "relation" && isFieldVisible(field, model.raw);
65
+ }));
66
+ const filters = {};
67
+ const knownQueryParameters = new Set(["page", "sort", "dir", "search"]);
68
+ for (const [parameterName, rawValue] of Object.entries(query)) {
69
+ if (knownQueryParameters.has(parameterName))
70
+ continue;
71
+ if (typeof rawValue !== "string")
72
+ throw new RequestValidationError(`Query parameter "${parameterName}" must be a single string value.`);
73
+ const rangeMatch = /^(.*)_(gte|lte)$/.exec(parameterName);
74
+ const fieldName = rangeMatch?.[1] ?? parameterName;
75
+ const field = fieldsByName.get(fieldName);
76
+ if (!field || !filterableFields.has(fieldName))
77
+ throw new RequestValidationError(`Filter "${parameterName}" is not allowed for this model.`);
78
+ if (rangeMatch) {
79
+ if (field.type !== "datetime")
80
+ throw new RequestValidationError(`Filter "${parameterName}" is only supported for date-time fields.`);
81
+ const operator = rangeMatch[2];
82
+ if (!operator)
83
+ throw new RequestValidationError(`Filter "${parameterName}" is invalid.`);
84
+ filters[fieldName] = { ...filters[fieldName], [operator]: parseFilterValue(field, rawValue) };
85
+ }
86
+ else {
87
+ filters[fieldName] = parseFilterValue(field, rawValue);
88
+ }
89
+ }
90
+ const searchValue = getQueryValue(query, "search")?.trim();
91
+ if (searchValue && searchValue.length > 200)
92
+ throw new RequestValidationError("Query parameter \"search\" must be 200 characters or fewer.");
93
+ const searchFields = model.resolved.searchFields.filter((fieldName) => {
94
+ const field = fieldsByName.get(fieldName);
95
+ return field?.type === "string" && isFieldVisible(field, model.raw);
96
+ });
97
+ const search = searchValue ? {
98
+ OR: searchFields.map((fieldName) => ({
99
+ [fieldName]: { contains: searchValue, ...(databaseProvider === "postgresql" ? { mode: "insensitive" } : {}) },
100
+ })),
101
+ } : undefined;
102
+ if (searchValue && searchFields.length === 0)
103
+ throw new RequestValidationError(`Model "${meta.name}" has no searchable fields.`);
104
+ return { page: pageValue, sort, dir, filters, search };
105
+ }
106
+ //# sourceMappingURL=listQuery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listQuery.js","sourceRoot":"","sources":["../src/listQuery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare const hashAdminPassword: (password: string) => Promise<string>;
2
+ export declare const verifyAdminPassword: (password: string, storedHash: string) => Promise<boolean>;
3
+ /** Always performs one password derivation, including when no account exists. */
4
+ export declare const verifyLoginPassword: (password: string, storedHash?: string) => Promise<boolean>;
@@ -0,0 +1,57 @@
1
+ import { randomBytes, scrypt as scryptCallback, timingSafeEqual } from "node:crypto";
2
+ const passwordPrefix = "scrypt";
3
+ const derivedKeyLength = 64;
4
+ const defaultParameters = { cost: 131_072, blockSize: 8, parallelism: 1 };
5
+ const maximumParameters = { cost: 131_072, blockSize: 16, parallelism: 4 };
6
+ function isValidCost(cost) {
7
+ return Number.isSafeInteger(cost) && cost >= 2 && cost <= maximumParameters.cost && (cost & (cost - 1)) === 0;
8
+ }
9
+ function parseStoredHash(storedHash) {
10
+ const [prefix, costValue, blockSizeValue, parallelismValue, salt, expectedValue] = storedHash.split("$");
11
+ if (prefix !== passwordPrefix || !costValue || !blockSizeValue || !parallelismValue || !salt || !expectedValue)
12
+ return null;
13
+ const parameters = { cost: Number(costValue), blockSize: Number(blockSizeValue), parallelism: Number(parallelismValue) };
14
+ if (!isValidCost(parameters.cost)
15
+ || !Number.isSafeInteger(parameters.blockSize)
16
+ || parameters.blockSize < 1
17
+ || parameters.blockSize > maximumParameters.blockSize
18
+ || !Number.isSafeInteger(parameters.parallelism)
19
+ || parameters.parallelism < 1
20
+ || parameters.parallelism > maximumParameters.parallelism)
21
+ return null;
22
+ const expected = Buffer.from(expectedValue, "base64url");
23
+ if (expected.length !== derivedKeyLength)
24
+ return null;
25
+ return { salt, expected, parameters };
26
+ }
27
+ async function derive(password, salt, parameters) {
28
+ const maxmem = (128 * parameters.cost * parameters.blockSize) + (128 * parameters.blockSize * parameters.parallelism) + 1_048_576;
29
+ return new Promise((resolve, reject) => {
30
+ const options = { N: parameters.cost, r: parameters.blockSize, p: parameters.parallelism, maxmem };
31
+ const scryptWithOptions = scryptCallback;
32
+ scryptWithOptions(password, salt, derivedKeyLength, options, (error, derivedKey) => {
33
+ if (error)
34
+ reject(error);
35
+ else
36
+ resolve(derivedKey);
37
+ });
38
+ });
39
+ }
40
+ export const hashAdminPassword = async (password) => {
41
+ const salt = randomBytes(16).toString("base64url");
42
+ const derived = await derive(password, salt, defaultParameters);
43
+ return [passwordPrefix, defaultParameters.cost, defaultParameters.blockSize, defaultParameters.parallelism, salt, derived.toString("base64url")].join("$");
44
+ };
45
+ export const verifyAdminPassword = async (password, storedHash) => {
46
+ const parsed = parseStoredHash(storedHash);
47
+ if (!parsed)
48
+ return false;
49
+ const actual = await derive(password, parsed.salt, parsed.parameters);
50
+ return timingSafeEqual(parsed.expected, actual);
51
+ };
52
+ const dummyHash = hashAdminPassword(randomBytes(32).toString("base64url"));
53
+ /** Always performs one password derivation, including when no account exists. */
54
+ export const verifyLoginPassword = async (password, storedHash) => {
55
+ return verifyAdminPassword(password, storedHash ?? await dummyHash);
56
+ };
57
+ //# sourceMappingURL=passwords.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passwords.js","sourceRoot":"","sources":["../src/passwords.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { AdminAction, AdminUser, ModelPermissions } from "./types.js";
2
+ /** Every model operation the admin can authorize. */
3
+ export type AdminOperation = "list" | "view" | "create" | "update" | "delete";
4
+ /**
5
+ * Decide whether an authenticated admin may perform a model operation.
6
+ *
7
+ * Reads remain available to authenticated admins by default. Writes and
8
+ * custom actions require an explicit allowlist. A configured empty role list
9
+ * denies everyone except a super admin.
10
+ */
11
+ export declare function hasModelPermission(adminUser: AdminUser, permissions: ModelPermissions, operation: AdminOperation): boolean;
12
+ /** Return the configured model-level role allowlist for a named custom action. */
13
+ export declare function hasActionPermission(adminUser: AdminUser, permissions: ModelPermissions, actionName: string): boolean;
14
+ /** Either allowlist authorizes an action; when both are present, both must allow the role. */
15
+ export declare function hasRegisteredActionPermission(adminUser: AdminUser, permissions: ModelPermissions, action: AdminAction): boolean;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Decide whether an authenticated admin may perform a model operation.
3
+ *
4
+ * Reads remain available to authenticated admins by default. Writes and
5
+ * custom actions require an explicit allowlist. A configured empty role list
6
+ * denies everyone except a super admin.
7
+ */
8
+ export function hasModelPermission(adminUser, permissions, operation) {
9
+ if (adminUser.isSuperAdmin)
10
+ return true;
11
+ const allowedRoles = permissions[operation];
12
+ if (allowedRoles === undefined)
13
+ return operation === "list" || operation === "view";
14
+ return allowedRoles.includes(adminUser.role);
15
+ }
16
+ /** Return the configured model-level role allowlist for a named custom action. */
17
+ export function hasActionPermission(adminUser, permissions, actionName) {
18
+ if (adminUser.isSuperAdmin)
19
+ return true;
20
+ const allowedRoles = permissions.actions?.[actionName];
21
+ if (allowedRoles === undefined)
22
+ return false;
23
+ return allowedRoles.includes(adminUser.role);
24
+ }
25
+ /** Either allowlist authorizes an action; when both are present, both must allow the role. */
26
+ export function hasRegisteredActionPermission(adminUser, permissions, action) {
27
+ if (adminUser.isSuperAdmin)
28
+ return true;
29
+ const modelAllowedRoles = permissions.actions?.[action.name];
30
+ const actionAllowedRoles = action.allowedRoles;
31
+ if (modelAllowedRoles === undefined && actionAllowedRoles === undefined)
32
+ return false;
33
+ return (modelAllowedRoles === undefined || modelAllowedRoles.includes(adminUser.role))
34
+ && (actionAllowedRoles === undefined || actionAllowedRoles.includes(adminUser.role));
35
+ }
36
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../src/permissions.ts"],"names":[],"mappings":"AASA;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAoB,EAAE,WAA6B,EAAE,SAAyB;IAC9G,IAAI,SAAS,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IAExC,MAAM,YAAY,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IAC5C,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,SAAS,KAAK,MAAM,IAAI,SAAS,KAAK,MAAM,CAAC;IAEpF,OAAO,YAAY,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AAChD,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,mBAAmB,CAAC,SAAoB,EAAE,WAA6B,EAAE,UAAkB;IACxG,IAAI,SAAS,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IAExC,MAAM,YAAY,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,UAAU,CAAC,CAAC;IACvD,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAE7C,OAAO,YAAY,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;AAChD,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,6BAA6B,CAAC,SAAoB,EAAE,WAA6B,EAAE,MAAmB;IACnH,IAAI,SAAS,CAAC,YAAY;QAAE,OAAO,IAAI,CAAC;IAExC,MAAM,iBAAiB,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7D,MAAM,kBAAkB,GAAG,MAAM,CAAC,YAAY,CAAC;IAC/C,IAAI,iBAAiB,KAAK,SAAS,IAAI,kBAAkB,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtF,OAAO,CAAC,iBAAiB,KAAK,SAAS,IAAI,iBAAiB,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;WAChF,CAAC,kBAAkB,KAAK,SAAS,IAAI,kBAAkB,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3F,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { DataAdapter } from "./adapter.js";
2
+ import type { FullRegisteredModel } from "./registry.js";
3
+ import type { AdminModelMeta, AdminUser } from "./types.js";
4
+ /** Build the safe select used by detail and edit endpoints. */
5
+ export declare function buildRecordSelect(meta: AdminModelMeta, model: FullRegisteredModel): Record<string, true | {
6
+ select: Record<string, true>;
7
+ }>;
8
+ /** Only return columns the list is configured to display, plus the primary key. */
9
+ export declare function buildListRecordSelect(meta: AdminModelMeta, model: FullRegisteredModel): Record<string, true | {
10
+ select: Record<string, true>;
11
+ }>;
12
+ /** Verify submitted belongs-to IDs are visible under the related model's own access rules. */
13
+ export declare function assertSelectedRelationsAreVisible(data: Record<string, unknown>, model: FullRegisteredModel, models: Map<string, FullRegisteredModel>, adapter: DataAdapter, adminUser: AdminUser): Promise<void>;