@rebasepro/server 0.13.1-canary.gf57a27e → 0.14.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 (126) hide show
  1. package/dist/{admin_block-CLeUMG8G.js → admin_block-H86dCPsj.js} +60 -2
  2. package/dist/admin_block-H86dCPsj.js.map +1 -0
  3. package/dist/api/ast-schema-editor.d.ts +74 -6
  4. package/dist/api/errors.d.ts +12 -0
  5. package/dist/api/rest/api-generator.d.ts +26 -1
  6. package/dist/api/rest/idempotency.d.ts +25 -4
  7. package/dist/api/rest/query-parser.d.ts +15 -1
  8. package/dist/api/rest/write-validation.d.ts +40 -4
  9. package/dist/auth/adapter-middleware.d.ts +3 -1
  10. package/dist/auth/admin-roles.d.ts +24 -0
  11. package/dist/auth/apple-oauth.d.ts +16 -11
  12. package/dist/auth/bitbucket-oauth.d.ts +2 -4
  13. package/dist/auth/builtin-auth-adapter.d.ts +11 -0
  14. package/dist/auth/discord-oauth.d.ts +5 -6
  15. package/dist/auth/facebook-oauth.d.ts +2 -4
  16. package/dist/auth/github-oauth.d.ts +2 -4
  17. package/dist/auth/gitlab-oauth.d.ts +6 -4
  18. package/dist/auth/google-oauth.d.ts +1 -0
  19. package/dist/auth/index.d.ts +7 -0
  20. package/dist/auth/interfaces.d.ts +81 -1
  21. package/dist/auth/jwt.d.ts +40 -0
  22. package/dist/auth/linkedin-oauth.d.ts +2 -4
  23. package/dist/auth/mfa-crypto.d.ts +54 -3
  24. package/dist/auth/mfa-gate.d.ts +42 -0
  25. package/dist/auth/mfa-routes.d.ts +26 -1
  26. package/dist/auth/mfa.d.ts +16 -0
  27. package/dist/auth/microsoft-oauth.d.ts +19 -4
  28. package/dist/auth/middleware.d.ts +26 -0
  29. package/dist/auth/oauth-code-flow.d.ts +66 -0
  30. package/dist/auth/oauth-signin-policy.d.ts +61 -0
  31. package/dist/auth/oidc-id-token.d.ts +61 -0
  32. package/dist/auth/rate-limiter.d.ts +62 -5
  33. package/dist/auth/registration-policy.d.ts +31 -0
  34. package/dist/auth/rls-scope.d.ts +25 -0
  35. package/dist/auth/routes.d.ts +15 -0
  36. package/dist/auth/slack-oauth.d.ts +2 -4
  37. package/dist/auth/spotify-oauth.d.ts +2 -4
  38. package/dist/auth/token-revocation.d.ts +38 -0
  39. package/dist/auth/twitter-oauth.d.ts +2 -5
  40. package/dist/{auth-BANhRhN9.js → auth-CYoPVf-E.js} +1814 -354
  41. package/dist/auth-CYoPVf-E.js.map +1 -0
  42. package/dist/{backup-BJiY_mM-.js → backup-C6ljYVTp.js} +2 -2
  43. package/dist/{backup-BJiY_mM-.js.map → backup-C6ljYVTp.js.map} +1 -1
  44. package/dist/{contract-routes-FSt0Nyfk.js → contract-routes-Bet-eCNJ.js} +3 -3
  45. package/dist/{contract-routes-FSt0Nyfk.js.map → contract-routes-Bet-eCNJ.js.map} +1 -1
  46. package/dist/cron/cron-loader.d.ts +26 -1
  47. package/dist/cron/cron-routes.d.ts +1 -1
  48. package/dist/cron/cron-scheduler.d.ts +5 -0
  49. package/dist/cron/define-cron.d.ts +14 -2
  50. package/dist/cron/index.d.ts +2 -2
  51. package/dist/{cron-loader-B1S2MCSl.js → cron-loader-YhhQeVBM.js} +43 -8
  52. package/dist/cron-loader-YhhQeVBM.js.map +1 -0
  53. package/dist/{cron-routes-D5a9v1HY.js → cron-routes-maM_RlUu.js} +10 -4
  54. package/dist/cron-routes-maM_RlUu.js.map +1 -0
  55. package/dist/{cron-scheduler-CsQtYLMq.js → cron-scheduler-DIpYBmZP.js} +13 -7
  56. package/dist/cron-scheduler-DIpYBmZP.js.map +1 -0
  57. package/dist/{cron-store-ZbjfuvMb.js → cron-store-Dvr4Y1sZ.js} +5 -5
  58. package/dist/{cron-store-ZbjfuvMb.js.map → cron-store-Dvr4Y1sZ.js.map} +1 -1
  59. package/dist/{ddl-bootstrap-Bp3mr6Td.js → ddl-bootstrap-BhXbTnBl.js} +2 -2
  60. package/dist/{ddl-bootstrap-Bp3mr6Td.js.map → ddl-bootstrap-BhXbTnBl.js.map} +1 -1
  61. package/dist/email/html.d.ts +54 -0
  62. package/dist/email/index.d.ts +3 -0
  63. package/dist/email/link-base.d.ts +39 -0
  64. package/dist/email/smtp-email-service.d.ts +7 -1
  65. package/dist/email/templates.d.ts +9 -1
  66. package/dist/email/types.d.ts +11 -1
  67. package/dist/env.d.ts +5 -0
  68. package/dist/{errors-BpudWAVU.js → errors-EBYiaJ2E.js} +14 -2
  69. package/dist/errors-EBYiaJ2E.js.map +1 -0
  70. package/dist/function-loader-DDS1v7YX.js +139 -0
  71. package/dist/function-loader-DDS1v7YX.js.map +1 -0
  72. package/dist/{function-routes-C0cLIy3N.js → function-routes-Btcez1T-.js} +18 -6
  73. package/dist/function-routes-Btcez1T-.js.map +1 -0
  74. package/dist/functions/define-function.d.ts +24 -5
  75. package/dist/functions/function-loader.d.ts +23 -0
  76. package/dist/functions/function-routes.d.ts +7 -1
  77. package/dist/functions/request-timeout.d.ts +34 -0
  78. package/dist/index.d.ts +3 -2
  79. package/dist/index.es.js +1437 -694
  80. package/dist/index.es.js.map +1 -1
  81. package/dist/init/docs.d.ts +10 -1
  82. package/dist/init/process-safety.d.ts +25 -0
  83. package/dist/init.d.ts +38 -0
  84. package/dist/{jwt-CzeviDcB.js → jwt-_IFqfTOg.js} +57 -9
  85. package/dist/jwt-_IFqfTOg.js.map +1 -0
  86. package/dist/logger-DfvF_8r-.js +190 -0
  87. package/dist/logger-DfvF_8r-.js.map +1 -0
  88. package/dist/{openapi-generator-VB0MUH8h.js → openapi-generator-DPKtUC9X.js} +87 -31
  89. package/dist/openapi-generator-DPKtUC9X.js.map +1 -0
  90. package/dist/request-timeout-RivJsME0.js +65 -0
  91. package/dist/request-timeout-RivJsME0.js.map +1 -0
  92. package/dist/schema-editor-routes-CRcS3ArS.js +437 -0
  93. package/dist/schema-editor-routes-CRcS3ArS.js.map +1 -0
  94. package/dist/serve-spa.d.ts +5 -4
  95. package/dist/services/outbound-url-guard.d.ts +53 -0
  96. package/dist/services/routed-realtime-service.d.ts +10 -2
  97. package/dist/services/webhook-service.d.ts +76 -1
  98. package/dist/singleton.d.ts +25 -8
  99. package/dist/{src-DVb5a0bp.js → src-C7rkDGxA.js} +536 -61
  100. package/dist/src-C7rkDGxA.js.map +1 -0
  101. package/dist/{src-Ca6NhxKs.js → src-Cz9nMgUR.js} +16 -3
  102. package/dist/src-Cz9nMgUR.js.map +1 -0
  103. package/dist/storage/LocalStorageController.d.ts +12 -1
  104. package/dist/storage/image-transform.d.ts +57 -0
  105. package/dist/storage/keys.d.ts +34 -1
  106. package/dist/storage/routes.d.ts +11 -0
  107. package/dist/storage/tus-handler.d.ts +22 -2
  108. package/dist/utils/logger.d.ts +18 -0
  109. package/package.json +6 -5
  110. package/dist/admin_block-CLeUMG8G.js.map +0 -1
  111. package/dist/auth-BANhRhN9.js.map +0 -1
  112. package/dist/cron-loader-B1S2MCSl.js.map +0 -1
  113. package/dist/cron-routes-D5a9v1HY.js.map +0 -1
  114. package/dist/cron-scheduler-CsQtYLMq.js.map +0 -1
  115. package/dist/errors-BpudWAVU.js.map +0 -1
  116. package/dist/function-loader-B_1fYfUY.js +0 -86
  117. package/dist/function-loader-B_1fYfUY.js.map +0 -1
  118. package/dist/function-routes-C0cLIy3N.js.map +0 -1
  119. package/dist/jwt-CzeviDcB.js.map +0 -1
  120. package/dist/logger-BYU66ENZ.js +0 -94
  121. package/dist/logger-BYU66ENZ.js.map +0 -1
  122. package/dist/openapi-generator-VB0MUH8h.js.map +0 -1
  123. package/dist/schema-editor-routes-BqjzvvWU.js +0 -248
  124. package/dist/schema-editor-routes-BqjzvvWU.js.map +0 -1
  125. package/dist/src-Ca6NhxKs.js.map +0 -1
  126. package/dist/src-DVb5a0bp.js.map +0 -1
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Refusal to send a request to a destination the guard does not allow.
3
+ *
4
+ * Distinct from a network error on purpose: a blocked destination is
5
+ * *terminal* — retrying it re-runs the same refusal — while an `ENOTFOUND`
6
+ * or a refused connection is worth another attempt.
7
+ */
8
+ export declare class BlockedUrlError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ export interface OutboundUrlGuardOptions {
12
+ /**
13
+ * Send to destinations that resolve to loopback, link-local, private or
14
+ * otherwise non-public addresses. Off by default, and turning it on
15
+ * re-opens SSRF: any caller who can choose the URL can then reach the
16
+ * pod's own metadata endpoint, the cluster API server and the database.
17
+ * Only for a genuinely local receiver (a sidecar, a dev tunnel).
18
+ */
19
+ allowPrivateNetworks?: boolean;
20
+ /**
21
+ * Hostname resolver. Defaults to `dns.lookup(host, { all: true })`.
22
+ * Injected by tests so a unit suite never touches a resolver.
23
+ */
24
+ lookup?: (hostname: string) => Promise<string[]>;
25
+ }
26
+ /**
27
+ * Why this address must not be a webhook destination, or `null` if it is an
28
+ * ordinary public address. Exported for tests and for anyone else adding an
29
+ * outbound call whose destination comes from data.
30
+ */
31
+ export declare function blockedAddressReason(address: string): string | null;
32
+ /**
33
+ * Resolve and validate a destination before anything connects to it.
34
+ *
35
+ * Three checks, in order, because each is cheaper than the next: the scheme,
36
+ * the hostname, then every address the hostname resolves to. *Every* address —
37
+ * a name that answers with one public and one private record is a rebinding
38
+ * attempt, and taking the first record would let it through.
39
+ *
40
+ * Known limit: this is validate-then-fetch. Between the lookup here and the
41
+ * connection the runtime makes there is a second resolution this code does not
42
+ * see, so a TTL-0 name that flips between answers can still land on a blocked
43
+ * address. Closing that needs the socket pinned to the address checked here,
44
+ * which Node's global `fetch` does not expose without an undici dispatcher.
45
+ * The redirect policy in {@link WebhookDispatcher} closes the much wider
46
+ * version of the same hole — a receiver that simply *points* at the internal
47
+ * address.
48
+ *
49
+ * @throws BlockedUrlError when the destination is not allowed. DNS failures
50
+ * are rethrown as they are, so callers can tell "never send this" from
51
+ * "could not send this right now".
52
+ */
53
+ export declare function assertAllowedOutboundUrl(rawUrl: string, options?: OutboundUrlGuardOptions): Promise<URL>;
@@ -15,6 +15,13 @@ interface ClientMessage {
15
15
  export interface WsRealtimeService extends RealtimeProvider {
16
16
  addClient(clientId: string, ws: unknown): void;
17
17
  handleClientMessage(clientId: string, message: ClientMessage, authContext?: unknown): Promise<void> | void;
18
+ /**
19
+ * Whether this provider actually implements channels, presence and
20
+ * broadcast. Declared by the provider rather than inferred, because the
21
+ * alternative — assuming the default one does — is how these frames came to
22
+ * be handed to an engine whose message switch drops them without a word.
23
+ */
24
+ supportsChannels?: boolean;
18
25
  }
19
26
  export interface RoutedRealtimeOptions {
20
27
  /** Per-engine realtime providers, keyed by data-source key. */
@@ -34,8 +41,9 @@ export interface RoutedRealtimeOptions {
34
41
  * - `subscribe_collection` / `subscribe_entity` → the provider for the
35
42
  * collection's data source (by `payload.path`).
36
43
  * - `unsubscribe` → forwarded to all providers (a no-op on non-owners).
37
- * - channel / presence / broadcast → the default provider (these are global
38
- * pub/sub, not bound to an engine).
44
+ * - channel / presence / broadcast → the first provider that declares
45
+ * `supportsChannels` (these are global pub/sub, not bound to an engine — but
46
+ * they are not implemented by every engine either).
39
47
  * - `addClient` and lifecycle (`onServerReady`/`destroy`/`stopListening`) →
40
48
  * all providers (each registers its own ws close handler for cleanup).
41
49
  */
@@ -16,14 +16,89 @@ export interface WebhookDeliveryResult {
16
16
  success: boolean;
17
17
  attemptNumber: number;
18
18
  }
19
+ export interface WebhookDispatcherOptions {
20
+ /**
21
+ * Deliver to destinations that resolve to loopback, link-local or private
22
+ * addresses. Off by default. Turning it on re-opens SSRF: anyone who can
23
+ * choose a webhook URL can then reach the pod's metadata endpoint, the
24
+ * cluster API server and the database — and read the first 1000 bytes of
25
+ * the answer back out of `responseBody`. Only for a receiver you run
26
+ * yourself on the same host or network.
27
+ */
28
+ allowPrivateNetworks?: boolean;
29
+ /**
30
+ * Deadline for one attempt, covering the response body as well as the
31
+ * headers. Defaults to 10s.
32
+ */
33
+ timeoutMs?: number;
34
+ /**
35
+ * Called with the final result of every delivery, including the ones
36
+ * started by {@link WebhookDispatcher.enqueueEntityChange} — which return
37
+ * nothing to their caller, so this is the only place their failures
38
+ * surface.
39
+ */
40
+ onDelivery?: (result: WebhookDeliveryResult) => void;
41
+ /**
42
+ * Hostname resolver used by the destination guard. Defaults to
43
+ * `dns.lookup`; injected by tests so a unit suite never asks a resolver.
44
+ */
45
+ lookup?: (hostname: string) => Promise<string[]>;
46
+ }
19
47
  export declare class WebhookDispatcher {
20
48
  private webhooks;
21
49
  private maxRetries;
22
50
  private retryDelays;
51
+ private readonly options;
52
+ private readonly timeoutMs;
53
+ private queue;
54
+ private draining;
55
+ constructor(options?: WebhookDispatcherOptions);
23
56
  /** Register webhooks to watch */
24
57
  setWebhooks(webhooks: WebhookConfig[]): void;
25
- /** Called when a entity changes — checks if any webhook matches */
58
+ /**
59
+ * Called when a entity changes — checks if any webhook matches, and awaits
60
+ * every delivery, retries included.
61
+ *
62
+ * **Do not await this inside a collection callback.** `afterSave` and
63
+ * `afterDelete` run inside the write's Postgres transaction, so awaiting a
64
+ * delivery here holds a pooled connection and the row's locks for as long
65
+ * as the receiver takes to answer — up to ~36s across three attempts, per
66
+ * matching webhook. Use {@link enqueueEntityChange} there; this method is
67
+ * for a custom function or a job that wants the results.
68
+ */
26
69
  onEntityChange(table: string, event: "INSERT" | "UPDATE" | "DELETE", id: string, entity: Record<string, unknown> | null, previousEntity?: Record<string, unknown> | null): Promise<WebhookDeliveryResult[]>;
70
+ /**
71
+ * Queue the same deliveries and return immediately.
72
+ *
73
+ * This is the form a collection callback wants. `afterSave` is awaited
74
+ * *inside* the transaction that wrote the row, so anything it awaits is
75
+ * transaction time; queueing hands the HTTP to a drain loop that runs
76
+ * after the callback returns, and the transaction commits without waiting
77
+ * for a third party. It also means a receiver that is down can no longer
78
+ * fail the customer's write.
79
+ *
80
+ * The cost, stated plainly: the queue is in-process and in-memory. A crash
81
+ * or a deploy between the enqueue and the delivery drops the event, and a
82
+ * receiver may see the notification a few milliseconds before the row it
83
+ * describes is committed. Use {@link flush} on shutdown, and
84
+ * `onDelivery` to record failures.
85
+ */
86
+ enqueueEntityChange(table: string, event: "INSERT" | "UPDATE" | "DELETE", id: string, entity: Record<string, unknown> | null, previousEntity?: Record<string, unknown> | null): void;
87
+ /**
88
+ * Wait for every queued delivery to finish. For graceful shutdown, and for
89
+ * tests that need to observe what {@link enqueueEntityChange} sent.
90
+ */
91
+ flush(): Promise<void>;
92
+ private startDraining;
93
+ private drainQueue;
94
+ /** The payload each matching webhook gets for this change. */
95
+ private buildDeliveries;
27
96
  private deliverWithRetry;
28
97
  private deliver;
98
+ /**
99
+ * Read at most {@link MAX_RESPONSE_BYTES} of the response and cancel the
100
+ * rest, so a receiver cannot buffer a 10 GB body into the pod before the
101
+ * caller truncates it to 1000 characters.
102
+ */
103
+ private readCappedBody;
29
104
  }
@@ -22,14 +22,30 @@ export declare function _resetRebaseMock(): void;
22
22
  * **Admin data plane** (`rebase.dataAsAdmin`):
23
23
  * Backed by the native DataDriver — calls go directly to the database without
24
24
  * JSON serialization, HTTP dispatch, or middleware overhead. The driver is
25
- * scoped as `{ uid: "service", roles: ["admin"] }`, so **every read and write
26
- * bypasses row-level-security policies**. No `REBASE_SERVICE_KEY` is required.
25
+ * scoped once as `{ uid: "service", roles: ["admin"] }` (`SERVICE_IDENTITY`),
26
+ * which makes it **admin-scoped, not RLS-bypassing**: every read and write runs
27
+ * in a transaction that has done `SET LOCAL ROLE rebase_user` with
28
+ * `app.uid = 'service'`, and policies are evaluated against that. No
29
+ * `REBASE_SERVICE_KEY` is required.
27
30
  *
28
- * ⚠️ Because it bypasses RLS, `rebase.dataAsAdmin` is for trusted background
29
- * work (cron jobs, migrations, service tasks) — **not** for serving user-facing
30
- * data. Inside a request handler, run user-scoped queries through the
31
- * request-scoped driver (`c.var.driver`), which carries the caller's identity
32
- * so RLS applies.
31
+ * It clears the default policies through their `rolesOverlap(['admin'])` arm —
32
+ * which is why the difference rarely shows. It shows when you write your own:
33
+ *
34
+ * - `policy.serverContext()` compiles to `auth.uid() IS NULL` and is therefore
35
+ * **false** for this accessor. A collection with `disableDefaultPolicies:
36
+ * true` whose rule is `serverContext()` denies these writes (`42501`) and
37
+ * returns zero rows — HTTP 200, empty — for these reads.
38
+ * - Its reach equals an `admin`-roled application user's reach. It is not a
39
+ * private door.
40
+ *
41
+ * `rebase.sql()` *is* an unconditional bypass — it runs on the owner connection
42
+ * and never goes through `withAuth`. Of the two accessors on this object, the
43
+ * quieter one is the more privileged.
44
+ *
45
+ * ⚠️ `rebase.dataAsAdmin` is for trusted background work (cron jobs,
46
+ * migrations, service tasks) — **not** for serving user-facing data. Inside a
47
+ * request handler, run user-scoped queries through the request-scoped driver
48
+ * (`c.var.driver`), which carries the caller's identity.
33
49
  *
34
50
  * `rebase.data` is **gone from the type**: `RebaseServerClient` omits it, so the
35
51
  * admin-scoped accessor has exactly one name and the privilege is visible at the
@@ -45,7 +61,8 @@ export declare function _resetRebaseMock(): void;
45
61
  * ```typescript
46
62
  * import { rebase } from "@rebasepro/server";
47
63
  *
48
- * // In a cron job, hook, or trusted service file (admin scope, bypasses RLS):
64
+ * // In a cron job, hook, or trusted service file (admin scope, RLS evaluated
65
+ * // as `{ uid: "service", roles: ["admin"] }`):
49
66
  * await rebase.email.send({ to: "admin@co.com", subject: "Alert", html: "<p>Hi</p>" });
50
67
  * const jobs = await rebase.dataAsAdmin.jobs.find({ limit: 10 });
51
68
  * ```