@seliseblocks/client 0.2.0 → 0.3.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.
@@ -0,0 +1,210 @@
1
+ import { BlocksApiError } from "../http/errors.js";
2
+ import { parseBody } from "../http/http-client.js";
3
+ import { trimSlashes } from "./route.js";
4
+ /** The public function route under the API gateway; the management API is CLI-only. */
5
+ const FUNCTION_API = "/logic/v4/fn";
6
+ const TERMINAL = new Set([
7
+ "SUCCEEDED", "FAILED", "TIMED_OUT", "CANCELLED", "RESOURCE_EXCEEDED", "OUTPUT_FAILED"
8
+ ]);
9
+ /** A run that finished without an answer: FAILED, TIMED_OUT, CANCELLED or RESOURCE_EXCEEDED. */
10
+ export class BlocksFunctionRunError extends Error {
11
+ runId;
12
+ status;
13
+ errorCode;
14
+ errorMessage;
15
+ constructor(runId, status, errorCode, errorMessage) {
16
+ super(`Function run ${runId} ended ${status}${errorCode ? ` (${errorCode})` : ""}${errorMessage ? `: ${errorMessage}` : ""}`);
17
+ this.runId = runId;
18
+ this.status = status;
19
+ this.errorCode = errorCode;
20
+ this.errorMessage = errorMessage;
21
+ this.name = "BlocksFunctionRunError";
22
+ }
23
+ }
24
+ /**
25
+ * Calling deployed Blocks functions from app code. Creating, saving, deploying and
26
+ * inspecting functions is CLI work (`blocks logic function *`), not exposed here.
27
+ */
28
+ export class BlocksFunctionsClient {
29
+ http;
30
+ apiUrl;
31
+ constructor(http, apiUrl) {
32
+ this.http = http;
33
+ this.apiUrl = apiUrl;
34
+ }
35
+ /**
36
+ * What: the absolute public URL of a function (optionally with a sub-path).
37
+ * Why: third parties (webhooks, other backends) call the function directly and need its address.
38
+ * How: callers send this project's `x-blocks-key`, plus a Blocks bearer token unless the trigger is Public.
39
+ */
40
+ url(functionId, path) {
41
+ return `${this.apiUrl}${routeOf(functionId, path)}`;
42
+ }
43
+ /**
44
+ * What: invokes a deployed function and returns either its answer or the queued run.
45
+ * Why: functions run async (202 + poll token) unless deployed as sync, and callers need to know which happened.
46
+ * How: `kind: "answered"` carries the function's own status/headers/body; `kind: "accepted"` carries
47
+ * `runId` + `pollToken` for `waitForResult`. Any non-2xx is thrown as `BlocksApiError` -- the platform's
48
+ * refusals (`FUNCTION_INVOKE_*` codes: 401/403/404/405/413/429/503), a failed or timed-out sync run
49
+ * (502/504 with `runId`), and a status the function chose itself. Use `call` for just the value.
50
+ */
51
+ async invoke(functionId, options = {}) {
52
+ const response = await this.invokeRaw(functionId, options);
53
+ const body = await parseBody(response);
54
+ if (!response.ok)
55
+ throw new BlocksApiError(response.status, response.statusText, body);
56
+ if (response.status === 202 && isRecord(body) && typeof body.runId === "string") {
57
+ const accepted = {
58
+ kind: "accepted",
59
+ status: 202,
60
+ runId: body.runId,
61
+ runStatus: String(body.status ?? "QUEUED")
62
+ };
63
+ if (typeof body.pollToken === "string")
64
+ accepted.pollToken = body.pollToken;
65
+ return accepted;
66
+ }
67
+ const runId = response.headers.get("x-blocks-run-id") ?? undefined;
68
+ return { kind: "answered", status: response.status, headers: response.headers, body: body, ...(runId ? { runId } : {}) };
69
+ }
70
+ /**
71
+ * What: invokes a function and returns the unread `Response`.
72
+ * Why: a streaming function (an `async function*` handler) sends its pieces as they come; reading
73
+ * them needs the body stream, which `invoke` buffers.
74
+ * How: same request as `invoke`. Pass `headers: { Accept: "text/event-stream" }` for Server-Sent
75
+ * Events (`data:` per piece, then `event: done` or `event: error`); otherwise the pieces arrive as
76
+ * chunked text. Never throws on a non-2xx status -- check `response.ok`.
77
+ */
78
+ invokeRaw(functionId, options = {}) {
79
+ const headers = new Headers(options.headers);
80
+ if (!headers.has("Accept"))
81
+ headers.set("Accept", "application/json");
82
+ const prefer = [
83
+ options.respondAsync ? "respond-async" : undefined,
84
+ options.waitSeconds !== undefined ? `wait=${Math.max(0, Math.floor(options.waitSeconds))}` : undefined
85
+ ].filter(Boolean);
86
+ if (prefer.length > 0)
87
+ headers.set("Prefer", prefer.join(", "));
88
+ return this.http.send(routeOf(functionId, options.path), {
89
+ auth: options.auth,
90
+ body: options.body,
91
+ headers,
92
+ method: options.method ?? "POST",
93
+ query: options.query,
94
+ signal: options.signal
95
+ });
96
+ }
97
+ /**
98
+ * What: reads a queued run's status and, once it finished, its result.
99
+ * Why: an async invocation answers 202 at once; this is how its outcome is collected.
100
+ * How: pass the `runId` and `pollToken` from the 202. No user session is needed -- the token is
101
+ * the capability -- and any wrong, expired or foreign token is the same 404.
102
+ */
103
+ async getResult(runId, pollToken, options = {}) {
104
+ const response = await this.http.send(`${FUNCTION_API}/runs/${encodeURIComponent(runId)}/result`, {
105
+ auth: false,
106
+ headers: { Accept: "application/json", "x-poll-token": pollToken },
107
+ signal: options.signal
108
+ });
109
+ const body = await parseBody(response);
110
+ if (!response.ok)
111
+ throw new BlocksApiError(response.status, response.statusText, body);
112
+ return body;
113
+ }
114
+ /**
115
+ * What: the full run record (input, result, attempts, timings) for a signed-in caller.
116
+ * Why: an admin screen or a caller without a poll token may need the whole run.
117
+ * How: requires the `blocks-logic::function::read` permission; reads within the caller's own tenant.
118
+ */
119
+ getRun(runId) {
120
+ return this.http.request(`${FUNCTION_API}/runs/${encodeURIComponent(runId)}`);
121
+ }
122
+ /**
123
+ * What: polls a queued run until it finishes.
124
+ * Why: turns the 202 + poll token into the final outcome without hand-written polling.
125
+ * How: resolves with the terminal `BlocksFunctionRunResult` (check `status`); rejects on `timeoutMs`
126
+ * (the run itself keeps going) or `signal` abort.
127
+ */
128
+ async waitForResult(accepted, options = {}) {
129
+ if (!accepted.pollToken) {
130
+ throw new Error(`Run ${accepted.runId} has no poll token; read it with getRun() (needs function::read) instead.`);
131
+ }
132
+ const interval = options.intervalMs ?? 1000;
133
+ const deadline = Date.now() + (options.timeoutMs ?? 60_000);
134
+ for (;;) {
135
+ const result = await this.getResult(accepted.runId, accepted.pollToken, { signal: options.signal });
136
+ if (TERMINAL.has(result.status))
137
+ return result;
138
+ if (Date.now() + interval > deadline) {
139
+ throw new Error(`Run ${accepted.runId} is still ${result.status}; stopped waiting (the run continues).`);
140
+ }
141
+ await delay(interval, options.signal);
142
+ }
143
+ }
144
+ /**
145
+ * What: invokes a function and returns its value, however it was deployed.
146
+ * Why: most callers want the handler's return value, not the transport details.
147
+ * How: a sync answer's body is returned as is; an async run is awaited and its result parsed. A
148
+ * returned `{ statusCode, headers, body }` is unwrapped to `body` (a 4xx/5xx `statusCode` throws
149
+ * `BlocksApiError`). A run that fails throws `BlocksFunctionRunError`.
150
+ */
151
+ async call(functionId, options = {}) {
152
+ const outcome = await this.invoke(functionId, options);
153
+ if (outcome.kind === "answered")
154
+ return outcome.body;
155
+ const run = await this.waitForResult(outcome, options);
156
+ if (run.status !== "SUCCEEDED" && run.status !== "OUTPUT_FAILED") {
157
+ throw new BlocksFunctionRunError(run.runId, run.status, run.errorCode, run.errorMessage);
158
+ }
159
+ const value = parseResult(run.result);
160
+ if (isHttpResponseShape(value)) {
161
+ if (value.statusCode >= 400)
162
+ throw new BlocksApiError(value.statusCode, "", value.body);
163
+ return value.body;
164
+ }
165
+ return value;
166
+ }
167
+ }
168
+ function routeOf(functionId, path) {
169
+ if (!functionId)
170
+ throw new Error("A function id is required.");
171
+ const suffix = trimSlashes(path ?? "");
172
+ const root = `${FUNCTION_API}/${encodeURIComponent(functionId)}`;
173
+ return suffix ? `${root}/${suffix}` : root;
174
+ }
175
+ function parseResult(result) {
176
+ if (result === null || result === undefined || result === "")
177
+ return undefined;
178
+ try {
179
+ return JSON.parse(result);
180
+ }
181
+ catch {
182
+ return result;
183
+ }
184
+ }
185
+ /** The handler's HTTP-answer shape (`FunctionHttpResponse`): `statusCode` plus only `headers`/`body`. */
186
+ function isHttpResponseShape(value) {
187
+ if (!isRecord(value) || typeof value.statusCode !== "number")
188
+ return false;
189
+ return Object.keys(value).every((key) => key === "statusCode" || key === "headers" || key === "body");
190
+ }
191
+ function isRecord(value) {
192
+ return typeof value === "object" && value !== null && !Array.isArray(value);
193
+ }
194
+ function delay(ms, signal) {
195
+ return new Promise((resolve, reject) => {
196
+ if (signal?.aborted) {
197
+ reject(signal.reason ?? new Error("Aborted"));
198
+ return;
199
+ }
200
+ const timer = setTimeout(() => {
201
+ signal?.removeEventListener("abort", onAbort);
202
+ resolve();
203
+ }, ms);
204
+ const onAbort = () => {
205
+ clearTimeout(timer);
206
+ reject(signal?.reason ?? new Error("Aborted"));
207
+ };
208
+ signal?.addEventListener("abort", onAbort, { once: true });
209
+ });
210
+ }
@@ -0,0 +1,13 @@
1
+ import { BlocksHttpClient } from "../http/http-client.js";
2
+ import { BlocksFunctionsClient } from "./functions-client.js";
3
+ import { BlocksProxyClient } from "./proxy-client.js";
4
+ /**
5
+ * blocks-logic runtime surface for app code: invoking deployed functions and calling
6
+ * third-party APIs through proxies. Authoring and administering both -- and workflows,
7
+ * schedules -- is CLI work.
8
+ */
9
+ export declare class BlocksLogicClient {
10
+ readonly functions: BlocksFunctionsClient;
11
+ readonly proxy: BlocksProxyClient;
12
+ constructor(http: BlocksHttpClient, apiUrl: string);
13
+ }
@@ -0,0 +1,15 @@
1
+ import { BlocksFunctionsClient } from "./functions-client.js";
2
+ import { BlocksProxyClient } from "./proxy-client.js";
3
+ /**
4
+ * blocks-logic runtime surface for app code: invoking deployed functions and calling
5
+ * third-party APIs through proxies. Authoring and administering both -- and workflows,
6
+ * schedules -- is CLI work.
7
+ */
8
+ export class BlocksLogicClient {
9
+ functions;
10
+ proxy;
11
+ constructor(http, apiUrl) {
12
+ this.functions = new BlocksFunctionsClient(http, apiUrl);
13
+ this.proxy = new BlocksProxyClient(http, apiUrl);
14
+ }
15
+ }
@@ -0,0 +1,33 @@
1
+ import { BlocksHttpClient } from "../http/http-client.js";
2
+ import { BlocksProxyRequestOptions } from "./types.js";
3
+ /**
4
+ * Calling a third-party API through a Blocks proxy, so the upstream URL and its
5
+ * credentials stay server-side.
6
+ */
7
+ export declare class BlocksProxyClient {
8
+ private readonly http;
9
+ private readonly apiUrl;
10
+ constructor(http: BlocksHttpClient, apiUrl: string);
11
+ /**
12
+ * What: the absolute gateway URL of a proxy route.
13
+ * Why: other backends and tools call the proxy directly.
14
+ * How: callers send this project's `x-blocks-key`, plus a Blocks bearer token unless the proxy is Public.
15
+ */
16
+ url(slug: string, path?: string): string;
17
+ /**
18
+ * What: calls the upstream through the proxy and returns the parsed response body.
19
+ * Why: app code reaches a third-party API without holding its URL or keys.
20
+ * How: `method` (default GET) must be one the proxy allows and `path` must match a declared route.
21
+ * Non-2xx -- the upstream's own errors, and gateway refusals with `PROXY_GATEWAY_<OUTCOME>` codes
22
+ * (401 no/invalid token, 403 access rules or undeclared route, 404 unknown or disabled proxy, 405
23
+ * method not allowed, 413 body over 1 MB, 422 body not mergeable, 429 rate limit with
24
+ * `Retry-After`, 502/503/504 upstream failure/unavailable/timeout) -- throws `BlocksApiError`.
25
+ */
26
+ request<T = unknown>(slug: string, options?: BlocksProxyRequestOptions): Promise<T>;
27
+ /**
28
+ * What: the same call, returning the unread `Response`.
29
+ * Why: for non-JSON upstream bodies (files, streams) or when headers such as `Retry-After` matter.
30
+ * How: never throws on a non-2xx status -- check `response.ok`.
31
+ */
32
+ requestRaw(slug: string, options?: BlocksProxyRequestOptions): Promise<Response>;
33
+ }
@@ -0,0 +1,64 @@
1
+ import { trimSlashes } from "./route.js";
2
+ /** A proxy's public gateway route; configuring proxies is CLI work (`blocks logic proxy *`). */
3
+ const PROXY_GATEWAY_API = "/logic/v4/proxy/gateway";
4
+ /**
5
+ * Calling a third-party API through a Blocks proxy, so the upstream URL and its
6
+ * credentials stay server-side.
7
+ */
8
+ export class BlocksProxyClient {
9
+ http;
10
+ apiUrl;
11
+ constructor(http, apiUrl) {
12
+ this.http = http;
13
+ this.apiUrl = apiUrl;
14
+ }
15
+ /**
16
+ * What: the absolute gateway URL of a proxy route.
17
+ * Why: other backends and tools call the proxy directly.
18
+ * How: callers send this project's `x-blocks-key`, plus a Blocks bearer token unless the proxy is Public.
19
+ */
20
+ url(slug, path) {
21
+ return `${this.apiUrl}${routeOf(slug, path)}`;
22
+ }
23
+ /**
24
+ * What: calls the upstream through the proxy and returns the parsed response body.
25
+ * Why: app code reaches a third-party API without holding its URL or keys.
26
+ * How: `method` (default GET) must be one the proxy allows and `path` must match a declared route.
27
+ * Non-2xx -- the upstream's own errors, and gateway refusals with `PROXY_GATEWAY_<OUTCOME>` codes
28
+ * (401 no/invalid token, 403 access rules or undeclared route, 404 unknown or disabled proxy, 405
29
+ * method not allowed, 413 body over 1 MB, 422 body not mergeable, 429 rate limit with
30
+ * `Retry-After`, 502/503/504 upstream failure/unavailable/timeout) -- throws `BlocksApiError`.
31
+ */
32
+ request(slug, options = {}) {
33
+ return this.http.request(routeOf(slug, options.path), {
34
+ auth: options.auth,
35
+ body: options.body,
36
+ headers: options.headers,
37
+ method: options.method ?? "GET",
38
+ query: options.query,
39
+ signal: options.signal
40
+ });
41
+ }
42
+ /**
43
+ * What: the same call, returning the unread `Response`.
44
+ * Why: for non-JSON upstream bodies (files, streams) or when headers such as `Retry-After` matter.
45
+ * How: never throws on a non-2xx status -- check `response.ok`.
46
+ */
47
+ requestRaw(slug, options = {}) {
48
+ return this.http.send(routeOf(slug, options.path), {
49
+ auth: options.auth,
50
+ body: options.body,
51
+ headers: options.headers,
52
+ method: options.method ?? "GET",
53
+ query: options.query,
54
+ signal: options.signal
55
+ });
56
+ }
57
+ }
58
+ function routeOf(slug, path) {
59
+ if (!slug)
60
+ throw new Error("A proxy slug is required.");
61
+ const suffix = trimSlashes(path ?? "");
62
+ const root = `${PROXY_GATEWAY_API}/${encodeURIComponent(slug)}`;
63
+ return suffix ? `${root}/${suffix}` : root;
64
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Drops leading and trailing `/` from a caller-supplied sub-path. A loop rather than
3
+ * `/^\/+|\/+$/g`: that pattern backtracks quadratically on a long run of slashes that
4
+ * does not end the string, and the path comes straight from app code.
5
+ */
6
+ export declare function trimSlashes(value: string): string;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Drops leading and trailing `/` from a caller-supplied sub-path. A loop rather than
3
+ * `/^\/+|\/+$/g`: that pattern backtracks quadratically on a long run of slashes that
4
+ * does not end the string, and the path comes straight from app code.
5
+ */
6
+ export function trimSlashes(value) {
7
+ let start = 0;
8
+ let end = value.length;
9
+ while (start < end && value[start] === "/")
10
+ start++;
11
+ while (end > start && value[end - 1] === "/")
12
+ end--;
13
+ return value.slice(start, end);
14
+ }
@@ -0,0 +1,95 @@
1
+ export type BlocksHttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
2
+ export type BlocksQueryValue = boolean | number | string | ReadonlyArray<boolean | number | string>;
3
+ /** One call to a function's public route. */
4
+ export type BlocksFunctionInvokeOptions = {
5
+ /**
6
+ * Request body. A plain value is sent as JSON; a `Blob`, `FormData`, `URLSearchParams`,
7
+ * `ArrayBuffer` or stream is sent as is. The handler sees it as `input.body`.
8
+ */
9
+ body?: unknown;
10
+ /**
11
+ * Extra request headers. The handler only sees an allow-list (content type, request ids,
12
+ * known webhook-signature headers) -- send app data in the body or query, not custom headers.
13
+ */
14
+ headers?: HeadersInit;
15
+ /** Defaults to POST. The trigger refuses verbs it does not list with 405. */
16
+ method?: BlocksHttpMethod;
17
+ /** Sub-path after the function id, passed to the handler as `input.path` (`orders/42`). */
18
+ path?: string;
19
+ query?: Record<string, BlocksQueryValue | undefined>;
20
+ /**
21
+ * Send the signed-in user's token (default true). A Token trigger needs it; pass
22
+ * `false` for a Public function called from a page with no session.
23
+ */
24
+ auth?: boolean;
25
+ /**
26
+ * Ask for the 202 + poll token even when the function is deployed as synchronous
27
+ * (`Prefer: respond-async`).
28
+ */
29
+ respondAsync?: boolean;
30
+ /**
31
+ * For a synchronous function, wait at most this many seconds for the answer
32
+ * (`Prefer: wait=<n>`); the platform caps the wait at ~30 s regardless. A call that
33
+ * outlives the wait is answered 202 + poll token, and the run still finishes.
34
+ */
35
+ waitSeconds?: number;
36
+ signal?: AbortSignal;
37
+ };
38
+ /** The run was queued and has not finished: collect it with `getResult`/`waitForResult`. */
39
+ export type BlocksFunctionAccepted = {
40
+ kind: "accepted";
41
+ status: 202;
42
+ runId: string;
43
+ /** Wire status: QUEUED, CLAIMED, STARTING, RUNNING or OUTPUT_PROCESSING. */
44
+ runStatus: string;
45
+ /**
46
+ * One-time capability for `getResult`. Absent only if the platform could not issue
47
+ * one; a caller with `function::read` can still use `getRun`.
48
+ */
49
+ pollToken?: string;
50
+ };
51
+ /** The function answered within the request (sync trigger), or the platform answered for it. */
52
+ export type BlocksFunctionAnswer<T = unknown> = {
53
+ kind: "answered";
54
+ status: number;
55
+ headers: Headers;
56
+ /** Parsed JSON when it parses, otherwise the text. */
57
+ body: T;
58
+ /** From `x-blocks-run-id` when the platform set it. */
59
+ runId?: string;
60
+ };
61
+ export type BlocksFunctionInvokeResult<T = unknown> = BlocksFunctionAccepted | BlocksFunctionAnswer<T>;
62
+ /** Wire statuses a run ends in. */
63
+ export type BlocksFunctionTerminalStatus = "SUCCEEDED" | "FAILED" | "TIMED_OUT" | "CANCELLED" | "RESOURCE_EXCEEDED" | "OUTPUT_FAILED";
64
+ /** What the poll route reports: status, and once finished, the outcome. Never the input. */
65
+ export type BlocksFunctionRunResult = {
66
+ runId: string;
67
+ /** Wire status, e.g. RUNNING or SUCCEEDED. */
68
+ status: string;
69
+ /** The handler's return value as JSON text, once the run has finished. */
70
+ result?: string | null;
71
+ errorCode?: string | null;
72
+ errorMessage?: string | null;
73
+ };
74
+ export type BlocksFunctionWaitOptions = {
75
+ /** Delay between polls. Default 1000 ms. */
76
+ intervalMs?: number;
77
+ /** Give up after this long. Default 60 000 ms; the run itself keeps going. */
78
+ timeoutMs?: number;
79
+ signal?: AbortSignal;
80
+ };
81
+ export type BlocksFunctionCallOptions = BlocksFunctionInvokeOptions & BlocksFunctionWaitOptions;
82
+ /** One call through a proxy's gateway route. */
83
+ export type BlocksProxyRequestOptions = {
84
+ /** JSON-serialized unless it is already a `BodyInit`. */
85
+ body?: unknown;
86
+ headers?: HeadersInit;
87
+ /** Defaults to GET. Must be one of the proxy's methods and match a declared route. */
88
+ method?: BlocksHttpMethod;
89
+ /** Route after the slug (`charges/ch_1`), matched against the proxy's declared routes. */
90
+ path?: string;
91
+ query?: Record<string, BlocksQueryValue | undefined>;
92
+ /** Send the signed-in user's token (default true). Pass `false` for a Public proxy. */
93
+ auth?: boolean;
94
+ signal?: AbortSignal;
95
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -12,7 +12,12 @@ export declare class BlocksNotifierClient {
12
12
  /**
13
13
  * What: pushes a notification through `POST /logic/v4/Notifier/Notify`.
14
14
  * Why: app code needs to notify specific users/roles, or everyone subscribed to a filter, in real time with an offline/persisted fallback.
15
- * How: target with at least one of `userIds`/`roles`/`subscriptionFilters`; set `saveDenormalizedPayloadAsAnObject` if `denormalizedPayload` is a JSON string clients should receive parsed.
15
+ * How: name a `configurationName` from the tenant's notification configurations, target with at least one of
16
+ * `userIds`/`roles`/`organizationIds`/`subscriptionFilters`, and pass `denormalizedPayload`; set
17
+ * `saveDenormalizedPayloadAsAnObject` if it is a JSON string clients should receive parsed. `connectionId`,
18
+ * `responseKey` and `responseValue` -- required strings server-side -- are filled with defaults when omitted.
19
+ * Call it as a signed-in user whose role has `blocks-logic::notifier::send`: since 2026-10-24 blocks-logic
20
+ * refuses a send (401) that has neither that permission nor the services' internal sender secret.
16
21
  */
17
22
  notify(request: BlocksNotifyRequest): Promise<BlocksNotifierPassThroughResponse>;
18
23
  /**
@@ -13,10 +13,16 @@ export class BlocksNotifierClient {
13
13
  /**
14
14
  * What: pushes a notification through `POST /logic/v4/Notifier/Notify`.
15
15
  * Why: app code needs to notify specific users/roles, or everyone subscribed to a filter, in real time with an offline/persisted fallback.
16
- * How: target with at least one of `userIds`/`roles`/`subscriptionFilters`; set `saveDenormalizedPayloadAsAnObject` if `denormalizedPayload` is a JSON string clients should receive parsed.
16
+ * How: name a `configurationName` from the tenant's notification configurations, target with at least one of
17
+ * `userIds`/`roles`/`organizationIds`/`subscriptionFilters`, and pass `denormalizedPayload`; set
18
+ * `saveDenormalizedPayloadAsAnObject` if it is a JSON string clients should receive parsed. `connectionId`,
19
+ * `responseKey` and `responseValue` -- required strings server-side -- are filled with defaults when omitted.
20
+ * Call it as a signed-in user whose role has `blocks-logic::notifier::send`: since 2026-10-24 blocks-logic
21
+ * refuses a send (401) that has neither that permission nor the services' internal sender secret.
17
22
  */
18
23
  notify(request) {
19
- return this.http.request(`${NOTIFIER_API}/Notify`, { body: request });
24
+ const body = { connectionId: "server", responseKey: "notification", responseValue: "notification", ...request };
25
+ return this.http.request(`${NOTIFIER_API}/Notify`, { body });
20
26
  }
21
27
  /**
22
28
  * What: reads unread notifications matching a subscription filter through `GET /logic/v4/Notifier/GetUnreadNotificationsBySubscriptionFilter`.
@@ -4,11 +4,21 @@ export type BlocksNotifierSubscriptionFilter = {
4
4
  value?: string;
5
5
  };
6
6
  export type BlocksNotifyRequest = {
7
- configurationName?: string;
7
+ /**
8
+ * The `name` of one of the tenant's notification configurations (created in the portal or
9
+ * with `blocks notification save`); it decides the channel and who may receive. Required.
10
+ */
11
+ configurationName: string;
12
+ /** The sender's own SignalR connection. Defaults to `"server"` -- a server-side send has none. */
8
13
  connectionId?: string;
9
14
  contentAvailable?: boolean;
10
- denormalizedPayload?: string;
15
+ /** The notification content, usually a JSON string. Required by the service. */
16
+ denormalizedPayload: string;
17
+ /** Members of these organizations receive it. */
18
+ organizationIds?: string[];
19
+ /** Label stored on the notification. Defaults to `"notification"`. */
11
20
  responseKey?: string;
21
+ /** Label stored on the notification. Defaults to `"notification"`. */
12
22
  responseValue?: string;
13
23
  roles?: string[];
14
24
  saveDenormalizedPayloadAsAnObject?: boolean;
package/dist/types.d.ts CHANGED
@@ -4,7 +4,9 @@ export type BlocksRequestOptions = {
4
4
  body?: unknown;
5
5
  headers?: HeadersInit;
6
6
  method?: string;
7
- query?: Record<string, boolean | number | string | undefined>;
7
+ /** A repeated key is an array (`?tag=a&tag=b`). */
8
+ query?: Record<string, boolean | number | string | ReadonlyArray<boolean | number | string> | undefined>;
9
+ signal?: AbortSignal;
8
10
  };
9
11
  export type BlocksExternalRequestOptions = {
10
12
  body?: BodyInit | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seliseblocks/client",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Framework-neutral TypeScript client SDK for SELISE Blocks frontend applications.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,6 +28,10 @@
28
28
  "types": "./dist/index.d.ts",
29
29
  "import": "./dist/index.js"
30
30
  },
31
+ "./functions": {
32
+ "types": "./dist/functions.d.ts",
33
+ "import": "./dist/functions.js"
34
+ },
31
35
  "./package.json": "./package.json"
32
36
  },
33
37
  "publishConfig": {