@protobase/server 0.6.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.
Files changed (105) hide show
  1. package/dist/assistant/conversations.d.ts +1 -1
  2. package/dist/assistant/tools.d.ts +1 -1
  3. package/dist/assistant/turn.d.ts +2 -2
  4. package/dist/audit/origin.js +12 -0
  5. package/dist/auth/jwt.d.ts +2 -0
  6. package/dist/auth/jwt.js +5 -3
  7. package/dist/batch/reorder.js +1 -1
  8. package/dist/better-auth/auth-schema.d.ts +4 -2
  9. package/dist/better-auth/auth-schema.js +18 -8
  10. package/dist/better-auth/authenticator.d.ts +7 -3
  11. package/dist/better-auth/authenticator.js +24 -3
  12. package/dist/better-auth/create-auth.d.ts +683 -10
  13. package/dist/better-auth/create-auth.js +72 -27
  14. package/dist/better-auth/organizations/accept-invitation.js +80 -0
  15. package/dist/better-auth/organizations/active-organization.js +12 -0
  16. package/dist/better-auth/organizations/admin-routes.js +83 -0
  17. package/dist/better-auth/organizations/app-roles.js +43 -0
  18. package/dist/better-auth/organizations/host.d.ts +53 -0
  19. package/dist/better-auth/organizations/host.js +251 -0
  20. package/dist/better-auth/organizations/invitation-endpoints.js +133 -0
  21. package/dist/better-auth/organizations/invitation-mail.js +17 -0
  22. package/dist/better-auth/organizations/invitation-token.js +27 -0
  23. package/dist/better-auth/organizations/member-endpoints.js +182 -0
  24. package/dist/better-auth/organizations/options.d.ts +48 -0
  25. package/dist/better-auth/organizations/options.js +23 -0
  26. package/dist/better-auth/organizations/organization-plugin.js +128 -0
  27. package/dist/better-auth/organizations/protobase-plugin.js +21 -0
  28. package/dist/better-auth/organizations/role-definitions.d.ts +15 -0
  29. package/dist/better-auth/organizations/role-definitions.js +56 -0
  30. package/dist/better-auth/organizations/session-organization.js +68 -0
  31. package/dist/better-auth/organizations/store.d.ts +19 -0
  32. package/dist/better-auth/organizations/store.js +73 -0
  33. package/dist/better-auth/policy-store.js +44 -35
  34. package/dist/better-auth/snake-case-migration.js +122 -0
  35. package/dist/better-auth/snake-case-names.js +72 -0
  36. package/dist/better-auth/staff-checks.js +5 -1
  37. package/dist/better-auth/staff-log.js +56 -45
  38. package/dist/better-auth/staff-sign-in-endpoints.js +1 -1
  39. package/dist/better-auth/status-route.js +14 -3
  40. package/dist/better-auth/users.d.ts +2 -1
  41. package/dist/better-auth/users.js +4 -1
  42. package/dist/create-admin.d.ts +8 -2
  43. package/dist/create-admin.js +31 -2
  44. package/dist/deps.d.ts +1 -0
  45. package/dist/files/attach.js +80 -0
  46. package/dist/files/cleanup-runner.d.ts +8 -0
  47. package/dist/files/cleanup-runner.js +58 -0
  48. package/dist/files/cleanup-schedule.d.ts +28 -0
  49. package/dist/files/cleanup-schedule.js +48 -0
  50. package/dist/files/detect.js +51 -0
  51. package/dist/files/download-url.js +43 -0
  52. package/dist/files/download.js +90 -0
  53. package/dist/files/duration.js +35 -0
  54. package/dist/files/file-cleanup.d.ts +18 -0
  55. package/dist/files/file-cleanup.js +17 -0
  56. package/dist/files/local-files.d.ts +17 -0
  57. package/dist/files/local-files.js +101 -0
  58. package/dist/files/media-types.js +99 -0
  59. package/dist/files/options.d.ts +15 -0
  60. package/dist/files/present.d.ts +15 -0
  61. package/dist/files/present.js +59 -0
  62. package/dist/files/processors.d.ts +15 -0
  63. package/dist/files/processors.js +39 -0
  64. package/dist/files/providers.js +70 -0
  65. package/dist/files/routes.js +26 -0
  66. package/dist/files/runtime.d.ts +3 -0
  67. package/dist/files/runtime.js +37 -0
  68. package/dist/files/signer.js +32 -0
  69. package/dist/files/store.d.ts +37 -0
  70. package/dist/files/ticket.js +87 -0
  71. package/dist/files/upload-body.js +85 -0
  72. package/dist/files/upload.js +124 -0
  73. package/dist/files/uri.js +47 -0
  74. package/dist/functions/define-function.d.ts +71 -0
  75. package/dist/functions/define-function.js +36 -0
  76. package/dist/functions/function-app.d.ts +17 -0
  77. package/dist/functions/function-app.js +19 -0
  78. package/dist/functions/read-json.d.ts +35 -0
  79. package/dist/functions/read-json.js +24 -0
  80. package/dist/functions/records.d.ts +43 -0
  81. package/dist/functions/records.js +31 -0
  82. package/dist/functions/require-env.d.ts +7 -0
  83. package/dist/functions/require-env.js +12 -0
  84. package/dist/functions/routes.js +60 -0
  85. package/dist/functions/verify-signature.d.ts +32 -0
  86. package/dist/functions/verify-signature.js +49 -0
  87. package/dist/index.d.ts +21 -3
  88. package/dist/index.js +13 -3
  89. package/dist/list-service.js +4 -2
  90. package/dist/openapi/resource-paths.js +78 -1
  91. package/dist/openapi/schemas.js +28 -3
  92. package/dist/project-config.d.ts +7 -1
  93. package/dist/project-config.js +37 -1
  94. package/dist/read-service.js +5 -1
  95. package/dist/request-access.d.ts +5 -0
  96. package/dist/request-access.js +9 -4
  97. package/dist/resource-source.d.ts +4 -1
  98. package/dist/routes/batch.js +1 -1
  99. package/dist/routes/collection.js +3 -1
  100. package/dist/routes/record.js +2 -5
  101. package/dist/types.d.ts +25 -5
  102. package/dist/write-pipeline.js +2 -1
  103. package/dist/write-shared.js +6 -3
  104. package/dist/write-update.js +2 -1
  105. package/package.json +6 -4
@@ -3,15 +3,19 @@ import { checkedFilter } from "./filter-input.js";
3
3
  import { tenantScope } from "./tenant.js";
4
4
  import { checkFilter, resolveAccess, restrictModel } from "@protobase/schema";
5
5
  //#region src/request-access.ts
6
+ const organizationOf = (session) => session.organization ? { organization: session.organization } : {};
6
7
  const withoutSensitive = (model) => ({
7
8
  ...model,
8
9
  fields: Object.fromEntries(Object.entries(model.fields).filter(([, field]) => !field.sensitive))
9
10
  });
10
11
  const requestAccess = async (deps, entry, session) => {
11
- const resolved = await resolveAccess(entry.source, { user: {
12
- id: session.user.id,
13
- roles: session.user.roles
14
- } }, deps.accessOptions);
12
+ const resolved = await resolveAccess(entry.source, {
13
+ user: {
14
+ id: session.user.id,
15
+ roles: session.user.roles
16
+ },
17
+ ...organizationOf(session)
18
+ }, deps.accessOptions);
15
19
  const readable = restrictModel(entry.model, resolved);
16
20
  return {
17
21
  entry,
@@ -86,6 +90,7 @@ const recordDecision = async (access, operation, record, input) => {
86
90
  id: session.user.id,
87
91
  roles: session.user.roles
88
92
  },
93
+ ...organizationOf(session),
89
94
  model: full,
90
95
  operation,
91
96
  record
@@ -1,5 +1,5 @@
1
1
  import { PageDefinition } from "@protobase/layout";
2
- import { AccessSource, UserMenuSource, ViewModel } from "@protobase/schema";
2
+ import { AccessSource, FileProcessor, UserMenuSource, ViewModel } from "@protobase/schema";
3
3
  import { z } from "zod";
4
4
  //#region src/resource-source.d.ts
5
5
  type FieldSource = {
@@ -7,6 +7,9 @@ type FieldSource = {
7
7
  meta: {
8
8
  readOnly: boolean;
9
9
  nullable: boolean;
10
+ file?: {
11
+ derive?: Record<string, FileProcessor>;
12
+ };
10
13
  };
11
14
  };
12
15
  /** What the server needs from a resource builder; every `resource(...)` chain satisfies it. */
@@ -1,6 +1,6 @@
1
+ import { parseParams } from "../params.js";
1
2
  import { batchBody } from "../batch/schema.js";
2
3
  import { runBatch } from "../batch/run-batch.js";
3
- import { parseParams } from "../params.js";
4
4
  import { Hono } from "hono";
5
5
  //#region src/routes/batch.ts
6
6
  /**
@@ -1,6 +1,7 @@
1
1
  import { runWrite } from "../write-pipeline.js";
2
- import { facets, numericHistogram, timeSeries } from "../aggregate-service.js";
3
2
  import { listRecords } from "../list-service.js";
3
+ import { facets, numericHistogram, timeSeries } from "../aggregate-service.js";
4
+ import { uploadFile } from "../files/upload.js";
4
5
  import { returnPreference } from "./preferences.js";
5
6
  import { resolveTarget, unknownAction } from "./target.js";
6
7
  import { Hono } from "hono";
@@ -31,6 +32,7 @@ const collectionRoutes = (deps) => {
31
32
  app.post("/:target", async (c) => {
32
33
  const { entry, action } = resolveTarget(deps, c.req.param("target"));
33
34
  const session = c.get("session");
35
+ if (action === "upload") return c.json(await uploadFile(deps, entry, session, c.req.raw), 201);
34
36
  const body = await c.req.json();
35
37
  if (action === "search") {
36
38
  const { body: page, warning } = await listRecords(deps, entry, session, body, searchFilterLength);
@@ -3,13 +3,13 @@ import { keyFromPath } from "../key.js";
3
3
  import { pickFields } from "../rows.js";
4
4
  import { readRecord } from "../read-service.js";
5
5
  import { runWrite } from "../write-pipeline.js";
6
+ import { auditOrigin } from "../audit/origin.js";
6
7
  import { deletedRows, selectedFields } from "../params.js";
7
8
  import { returnPreference } from "./preferences.js";
8
9
  import { resolveTarget } from "./target.js";
9
10
  import { revealField } from "../reveal-service.js";
10
11
  import { Hono } from "hono";
11
12
  //#region src/routes/record.ts
12
- const headerValue = (name, value) => value === void 0 ? {} : { [name]: value };
13
13
  const recordRoutes = (deps) => {
14
14
  const app = new Hono();
15
15
  app.get("/:resource/:key", async (c) => {
@@ -31,10 +31,7 @@ const recordRoutes = (deps) => {
31
31
  const revealed = await revealField(deps, entry, c.get("session"), {
32
32
  key: keyFromPath(c, entry, ":reveal"),
33
33
  field: typeof body === "object" && body !== null ? body.field : void 0,
34
- origin: {
35
- ...headerValue("userAgent", c.req.header("user-agent")),
36
- ...headerValue("forwardedFor", c.req.header("x-forwarded-for"))
37
- }
34
+ origin: auditOrigin(c.req.raw.headers)
38
35
  });
39
36
  return c.json(revealed, 200, { "Cache-Control": "no-store" });
40
37
  };
package/dist/types.d.ts CHANGED
@@ -8,9 +8,14 @@ export type User = {
8
8
  id: string | number;
9
9
  roles: string[];
10
10
  };
11
+ /** The organization a request works in, with organizations on: the same value as `tenant`. */
12
+ export type SessionOrganization = {
13
+ id: TenantValue;
14
+ };
11
15
  export type Session = {
12
16
  user: User;
13
17
  tenant?: TenantValue;
18
+ organization?: SessionOrganization;
14
19
  };
15
20
  /** Resolves the caller of a request, or throws a 401 problem (see `unauthorized`). */
16
21
  export type Authenticator = (request: Request) => Promise<Session> | Session;
@@ -32,8 +37,13 @@ export type WriteEvent = {
32
37
  };
33
38
  /** Extension point of the write pipeline: a hook that throws rolls the write back. */
34
39
  export type PipelineHook = (event: WriteEvent, trx: Db) => Promise<void>;
40
+ /** Where a request came from, as its headers say; proxies set `forwardedFor`, so it is only as trustworthy as they are. */
41
+ export type AuditOrigin = {
42
+ userAgent?: string;
43
+ forwardedFor?: string;
44
+ };
35
45
  /** What happened, by whom, to which value. It never carries the value itself. */
36
- export type AuditEvent = {
46
+ export type FieldRevealedEvent = {
37
47
  type: 'field.revealed';
38
48
  /** ISO 8601, when the server answered. */
39
49
  at: string;
@@ -47,12 +57,22 @@ export type AuditEvent = {
47
57
  /** The record's key as its URL has it (composite keys joined with a comma). */
48
58
  recordKey: string;
49
59
  field: string;
50
- /** Where the request came from, as its headers say; proxies set `forwardedFor`, so it is only as trustworthy as they are. */
51
- origin: {
52
- userAgent?: string;
53
- forwardedFor?: string;
60
+ origin: AuditOrigin;
61
+ };
62
+ /** Someone with a global role started working in an organization they are not a member of. */
63
+ export type OrganizationEnteredEvent = {
64
+ type: 'organization.entered';
65
+ /** ISO 8601, when the server answered. */
66
+ at: string;
67
+ /** Their global roles, which let them in. */
68
+ actor: {
69
+ id: string | number;
70
+ roles: string[];
54
71
  };
72
+ organization: TenantValue;
73
+ origin: AuditOrigin;
55
74
  };
75
+ export type AuditEvent = FieldRevealedEvent | OrganizationEnteredEvent;
56
76
  /**
57
77
  * Where audit events go. `publish` resolves once the event is accepted; when it rejects, the request that caused the
58
78
  * event fails, so nothing is revealed without its event. A real queue replaces the console one through
@@ -3,6 +3,7 @@ import { deleteRecord, fetchRecord, insertRecord, restoreRecord } from "./record
3
3
  import { tenantScope } from "./tenant.js";
4
4
  import { requestAccess, requireOperation, targetFilter, writableFields } from "./request-access.js";
5
5
  import { writeTransaction } from "./transactions.js";
6
+ import { attachFiles } from "./files/attach.js";
6
7
  import { validateBody } from "./validate-body.js";
7
8
  import { assertWritten, checkPrecondition, decide, denied, firstKeyField, missing, notifier, shown } from "./write-shared.js";
8
9
  import { applyUpdate, planUpdate } from "./write-update.js";
@@ -22,7 +23,7 @@ const create = async (trx, deps, access, body) => {
22
23
  const tenantValues = full.tenant && scope ? { [full.tenant]: scope.tenantValue } : {};
23
24
  const preset = typeof body === "object" && body !== null && !Array.isArray(body) && writableFields(access, "create").includes(full.owner ?? "") ? withOwner(access, body) : body;
24
25
  const row = {
25
- ...withOwner(access, validateBody(access, "create", preset, tenantValues)),
26
+ ...await attachFiles(access, withOwner(access, validateBody(access, "create", preset, tenantValues))),
26
27
  ...tenantValues
27
28
  };
28
29
  const filter = await decide(access, "create", void 0, row);
@@ -1,9 +1,11 @@
1
1
  import { forbidden, notFound, preconditionFailed, preconditionRequired } from "./problem.js";
2
+ import { presentFiles } from "./files/present.js";
2
3
  import { ifMatchSatisfied } from "./etag.js";
3
4
  import { pickFields } from "./rows.js";
4
5
  import { fetchRecord } from "./records.js";
5
6
  import { tenantScope } from "./tenant.js";
6
7
  import { andFilters, recordDecision } from "./request-access.js";
8
+ import { scheduleRemovedFiles } from "./files/attach.js";
7
9
  //#region src/write-shared.ts
8
10
  const missing = (entry) => notFound(`No ${entry.name} with this key`);
9
11
  const checkPrecondition = (current, ifMatch, required) => {
@@ -30,9 +32,9 @@ const assertWritten = async (trx, access, written, operation, ...filters) => {
30
32
  fields: [firstKeyField(access)]
31
33
  })) throw denied(access.entry, `leave in that state (${operation})`);
32
34
  };
33
- /** The caller sees the readable fields only; hooks get the whole record. */
34
- const shown = (access, written) => ({
35
- record: pickFields(written.record, Object.keys(access.model.fields)),
35
+ /** The caller sees the readable fields only, files as objects with their URL; hooks get the whole record. */
36
+ const shown = async (access, written) => ({
37
+ record: (await presentFiles(access, [pickFields(written.record, Object.keys(access.model.fields))]))[0],
36
38
  etag: written.etag
37
39
  });
38
40
  /** Runs the pipeline hooks for one write, inside the transaction. */
@@ -47,6 +49,7 @@ const notifier = (deps, access) => async (trx, kind, before, after) => {
47
49
  ...after && { after }
48
50
  };
49
51
  for (const hook of deps.hooks) await hook(event, trx);
52
+ await scheduleRemovedFiles(access, kind, before, after);
50
53
  };
51
54
  //#endregion
52
55
  export { assertWritten, checkPrecondition, decide, denied, firstKeyField, keyOf, missing, notifier, shown };
@@ -1,6 +1,7 @@
1
1
  import { fetchRecord, updateRecord } from "./records.js";
2
2
  import { tenantScope } from "./tenant.js";
3
3
  import { targetFilter } from "./request-access.js";
4
+ import { attachFiles } from "./files/attach.js";
4
5
  import { validateBody } from "./validate-body.js";
5
6
  import { assertWritten, checkPrecondition, decide, denied, firstKeyField, missing, notifier } from "./write-shared.js";
6
7
  //#region src/write-update.ts
@@ -20,7 +21,7 @@ const planUpdate = async (trx, access, key, body, ifMatch, required = true) => {
20
21
  });
21
22
  if (!current) throw missing(entry);
22
23
  checkPrecondition(current, ifMatch, required);
23
- const values = validateBody(access, "update", body, current.record);
24
+ const values = await attachFiles(access, validateBody(access, "update", body, current.record), current.record);
24
25
  const filter = Object.keys(values).length === 0 ? void 0 : await decide(access, "update", current.record, values);
25
26
  if (filter && !await fetchRecord(trx, entry, key, tenant, {
26
27
  filter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
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,13 +30,15 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@better-auth/passkey": "^1.7.7",
33
- "@protobase/layout": "0.6.0",
34
- "@protobase/query": "0.6.0",
35
- "@protobase/schema": "0.6.0",
33
+ "@protobase/layout": "0.8.0",
34
+ "@protobase/query": "0.8.0",
35
+ "@protobase/schema": "0.8.0",
36
36
  "@scalar/hono-api-reference": "^0.12.10",
37
37
  "@simplewebauthn/server": "^13.3.1",
38
38
  "better-auth": "^1.7.7",
39
+ "file-type": "^22.1.1",
39
40
  "hono": "^4.13.13",
41
+ "image-size": "^2.0.4",
40
42
  "jose": "^6.2.12",
41
43
  "kysely": "^0.29.6",
42
44
  "nodemailer": "^10.0.16",