@seliseblocks/client 0.1.5 → 0.2.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.
@@ -1,11 +1,11 @@
1
1
  import { BlocksAuthenticationClient } from "../auth/auth-client.js";
2
- import { RequiredConfig } from "../client.js";
2
+ import { BlocksResolvedConfig } from "../client.js";
3
3
  import { BlocksExternalRequestOptions, BlocksRequestOptions } from "../types.js";
4
4
  export declare class BlocksHttpClient {
5
5
  private readonly config;
6
6
  private readonly auth;
7
7
  private readonly fetchImpl;
8
- constructor(config: RequiredConfig, auth: BlocksAuthenticationClient, fetchImpl?: typeof fetch);
8
+ constructor(config: BlocksResolvedConfig, auth: BlocksAuthenticationClient, fetchImpl?: typeof fetch);
9
9
  request<T>(path: string, options?: BlocksRequestOptions): Promise<T>;
10
10
  external<T = unknown>(url: string, options?: BlocksExternalRequestOptions): Promise<T>;
11
11
  }
@@ -12,26 +12,38 @@ export class BlocksHttpClient {
12
12
  }
13
13
  async request(path, options = {}) {
14
14
  const url = buildUrl(this.config.apiUrl, path, options.query);
15
- const headers = new Headers(options.headers);
16
15
  const rawBody = isBodyInit(options.body) ? options.body : undefined;
17
- headers.set("Accept", "application/json");
18
- headers.set("x-blocks-key", this.config.xBlocksKey);
19
- if (options.body !== undefined && !rawBody && !headers.has("Content-Type")) {
20
- headers.set("Content-Type", "application/json");
16
+ const send = async (accessTokenOverride) => {
17
+ const headers = new Headers(options.headers);
18
+ headers.set("Accept", "application/json");
19
+ headers.set("x-blocks-key", this.config.xBlocksKey);
20
+ if (options.body !== undefined && !rawBody && !headers.has("Content-Type")) {
21
+ headers.set("Content-Type", "application/json");
22
+ }
23
+ if (options.auth !== false) {
24
+ const token = accessTokenOverride ?? options.accessToken ?? await this.auth.accessToken();
25
+ if (token)
26
+ headers.set("Authorization", `Bearer ${token}`);
27
+ }
28
+ return this.fetchImpl(url, {
29
+ body: rawBody ?? (options.body === undefined ? undefined : JSON.stringify(options.body)),
30
+ // IAM's hosted IdP flow sets the session as a Secure, httpOnly cookie by default;
31
+ // without this the browser never sends it back on subsequent Blocks API calls.
32
+ credentials: "include",
33
+ headers,
34
+ method: options.method ?? (options.body === undefined ? "GET" : "POST")
35
+ });
36
+ };
37
+ let response = await send();
38
+ // A 401 on a token the caller supplied directly (options.accessToken) is the
39
+ // caller's problem to resolve, not ours. Otherwise, give the app one chance to
40
+ // recover through its own onUnauthorized hook (expected to dedupe concurrent
41
+ // callers behind a single refresh) and retry exactly once with what it returns.
42
+ if (response.status === 401 && options.auth !== false && options.accessToken === undefined && this.config.onUnauthorized) {
43
+ const freshToken = await this.config.onUnauthorized();
44
+ if (freshToken)
45
+ response = await send(freshToken);
21
46
  }
22
- if (options.auth !== false) {
23
- const token = options.accessToken ?? await this.auth.accessToken();
24
- if (token)
25
- headers.set("Authorization", `Bearer ${token}`);
26
- }
27
- const response = await this.fetchImpl(url, {
28
- body: rawBody ?? (options.body === undefined ? undefined : JSON.stringify(options.body)),
29
- // IAM's hosted IdP flow sets the session as a Secure, httpOnly cookie by default;
30
- // without this the browser never sends it back on subsequent Blocks API calls.
31
- credentials: "include",
32
- headers,
33
- method: options.method ?? (options.body === undefined ? "GET" : "POST")
34
- });
35
47
  const body = await parseBody(response);
36
48
  if (!response.ok)
37
49
  throw new BlocksApiError(response.status, response.statusText, body);
@@ -49,6 +61,23 @@ export class BlocksHttpClient {
49
61
  return body;
50
62
  }
51
63
  }
64
+ /**
65
+ * `request` attaches the caller's bearer token, `x-blocks-key`, and
66
+ * `credentials: "include"`, so it must only ever reach the configured Blocks
67
+ * API. An absolute URL is still accepted -- callers legitimately pass a full
68
+ * API URL -- but one pointing somewhere else would hand a live session to a
69
+ * third party, so it is refused with a pointer to `external`, which sends no
70
+ * Blocks credentials by design.
71
+ */
72
+ function sameOriginOnly(path, baseUrl) {
73
+ const target = new URL(path);
74
+ const base = new URL(`${baseUrl}/`);
75
+ if (target.origin !== base.origin) {
76
+ throw new Error(`Refusing to send Blocks credentials to ${target.origin}: it is not the configured apiUrl (${base.origin}). `
77
+ + "Use blocks.http.external(url) for third-party URLs such as a pre-signed storage upload.");
78
+ }
79
+ return target;
80
+ }
52
81
  function isBodyInit(value) {
53
82
  return typeof Blob !== "undefined" && value instanceof Blob
54
83
  || typeof FormData !== "undefined" && value instanceof FormData
@@ -59,7 +88,7 @@ function isBodyInit(value) {
59
88
  }
60
89
  function buildUrl(baseUrl, path, query) {
61
90
  const url = /^https?:\/\//i.test(path)
62
- ? new URL(path)
91
+ ? sameOriginOnly(path, baseUrl)
63
92
  : new URL(path.replace(/^\/+/, ""), `${baseUrl}/`);
64
93
  for (const [key, value] of Object.entries(query ?? {})) {
65
94
  if (value !== undefined)
@@ -73,9 +102,13 @@ async function parseBody(response) {
73
102
  const text = await response.text();
74
103
  if (!text)
75
104
  return undefined;
76
- const contentType = response.headers.get("content-type") ?? "";
77
- if (contentType.includes("application/json"))
78
- return JSON.parse(text);
105
+ // Parsing is best-effort for every content type, including a declared
106
+ // application/json. A proxy, WAF, or load balancer in front of the API can
107
+ // answer with an HTML error page while still claiming JSON; throwing a raw
108
+ // SyntaxError there would lose the HTTP status entirely and break the
109
+ // documented contract that a non-2xx response always surfaces as a
110
+ // BlocksApiError. Returning the raw text keeps the status and hands the
111
+ // caller the body it actually got.
79
112
  try {
80
113
  return JSON.parse(text);
81
114
  }
package/dist/index.d.ts CHANGED
@@ -1,17 +1,20 @@
1
1
  export { createBlocksClient } from "./client.js";
2
2
  export { BlocksAuthenticationClient } from "./auth/auth-client.js";
3
3
  export { BlocksDataClient } from "./data/data-client.js";
4
- export type { BlocksClient, BlocksClientConfig, BlocksOidcConfig } from "./client.js";
4
+ export type { BlocksClient, BlocksClientConfig, BlocksOidcConfig, BlocksPublicConfig, BlocksResolvedConfig,
5
+ /** @deprecated Renamed to `BlocksResolvedConfig`. */
6
+ RequiredConfig } from "./client.js";
5
7
  export { BlocksApiError } from "./http/errors.js";
8
+ export type { BlocksHttpClient } from "./http/http-client.js";
6
9
  export { BlocksIAMClient } from "./iam/iam-client.js";
7
10
  export { BlocksLocalizationClient } from "./localization/localization-client.js";
8
11
  export { BlocksMailClient } from "./mail/mail-client.js";
9
12
  export { BlocksMfaClient } from "./mfa/mfa-client.js";
10
13
  export { BlocksNotifierClient } from "./notifier/notifier-client.js";
11
- export type { BlocksAuthPassThroughResponse, BlocksAuthResponse, BlocksClientCredentialsRequest, BlocksAuthLoginRequest, BlocksIdpCallbackRequest, BlocksIdpInitiateRequest, BlocksIdpInitiateResponse, BlocksLoginOption, BlocksLogoutRequest, BlocksOidcRefreshRequest, BlocksOidcUserInfo, BlocksRefreshRequest, BlocksSocialLoginRequest, BlocksSwitchOrganizationRequest } from "./auth/auth-client.js";
12
- export type { BlocksRequestOptions } from "./types.js";
14
+ export type { BlocksAuthJsonOptions, BlocksAuthPassThroughResponse, BlocksAuthResponse, BlocksClientCredentialsRequest, BlocksAuthLoginRequest, BlocksIdpCallbackRequest, BlocksIdpInitiateRequest, BlocksIdpInitiateResponse, BlocksLoginOption, BlocksLogoutRequest, BlocksOidcRefreshRequest, BlocksOidcUserInfo, BlocksRefreshRequest, BlocksSocialLoginRequest, BlocksSwitchOrganizationRequest } from "./auth/auth-client.js";
15
+ export type { BlocksExternalRequestOptions, BlocksRequestOptions } from "./types.js";
13
16
  export type { BlocksBaseResponse, BlocksGetMyOrganizationsResponse, BlocksGetOrganizationResponse, BlocksGetOrganizationsResponse, BlocksGetPermissionResponse, BlocksGetPermissionsResponse, BlocksGetRoleResponse, BlocksGetRolesResponse, BlocksGetUserResponse, BlocksGetUsersResponse, BlocksIamListRequest, BlocksMeResponse, BlocksOrganization, BlocksPermission, BlocksQueryListResponse, BlocksQueryResponse, BlocksRole, BlocksUser } from "./iam/types.js";
14
- export type { BlocksDataCollection, BlocksDataListOptions, BlocksDirectoryCreateRequest, BlocksDirectoryDeleteRequest, BlocksDirectoryMoveRequest, BlocksDirectoryUpdateRequest, BlocksFileCopyRequest, BlocksFileCreateVersionRequest, BlocksFileDeleteRequest, BlocksFileGetOptions, BlocksFileInfoListRequest, BlocksFileListRequest, BlocksFileMoveRequest, BlocksFileRenameRequest, BlocksFileUpdateAdditionalInfoRequest, BlocksFileUploadRequest, BlocksFileVersionsRequest, BlocksGraphqlRequest, BlocksLocalStorageUploadRequest, BlocksPagedResult, BlocksRegexGenerateRequest, BlocksSchemaFieldValidationOptions, BlocksSchemaListOptions, BlocksSchemaValidationListOptions, BlocksStorageAccessPolicyRequest, BlocksStorageEffect, BlocksStorageObject, BlocksStorageObjectListRequest, BlocksStorageObjectPageRequest, BlocksStorageObjectSearchRequest, BlocksStorageObjectsResponse, BlocksStorageObjectType, BlocksStoragePermission, BlocksStoragePermissionFlags, BlocksStoragePrincipalType, BlocksStorageResourceRequest, BlocksStorageResourceType, BlocksStorageRevokeAccessRequest, BlocksStorageShareRequest, BlocksStorageToggleInheritanceRequest, BlocksUploadToUrlRequest } from "./data/types.js";
17
+ export type { BlocksDataCollection, BlocksDataCollectionOptions, BlocksDataDeleteOptions, BlocksDataGetOptions, BlocksDataListOptions, BlocksDataSchema, BlocksDirectoryCreateRequest, BlocksDirectoryDeleteRequest, BlocksDirectoryMoveRequest, BlocksDirectoryUpdateRequest, BlocksFileCopyRequest, BlocksFileCreateVersionRequest, BlocksFileDeleteRequest, BlocksFileGetOptions, BlocksFileInfoListRequest, BlocksFileListRequest, BlocksFileMoveRequest, BlocksFileRenameRequest, BlocksFileUpdateAdditionalInfoRequest, BlocksFileUploadRequest, BlocksFileVersionsRequest, BlocksGraphqlRequest, BlocksLocalStorageUploadRequest, BlocksPagedResult, BlocksRegexGenerateRequest, BlocksSchemaFieldValidationOptions, BlocksSchemaListOptions, BlocksSchemaValidationListOptions, BlocksStorageAccessPolicyRequest, BlocksStorageEffect, BlocksStorageObject, BlocksStorageObjectListRequest, BlocksStorageObjectPageRequest, BlocksStorageObjectSearchRequest, BlocksStorageObjectsResponse, BlocksStorageObjectType, BlocksStoragePermission, BlocksStoragePermissionFlags, BlocksStoragePrincipalType, BlocksStorageResourceRequest, BlocksStorageResourceType, BlocksStorageRevokeAccessRequest, BlocksStorageShareRequest, BlocksStorageToggleInheritanceRequest, BlocksUploadToUrlRequest } from "./data/types.js";
15
18
  export type { BlocksLocalizationDictionary, BlocksLocalizationKey, BlocksLocalizationKeyResource, BlocksLocalizationKeysByNamesRequest, BlocksLocalizationKeysByNamesResponse, BlocksLocalizationLanguage, BlocksLocalizationModule, BlocksLocalizationTranslationOptions } from "./localization/types.js";
16
19
  export type { BlocksMfaBackupCodeUseRequest, BlocksMfaConfig, BlocksMfaConfigSaveRequest, BlocksMfaGenerateRequest, BlocksMfaPassThroughResponse, BlocksMfaResendRequest, BlocksMfaSetMethodRequest, BlocksMfaVerifyRequest, BlocksMfaVerifyTotpSetupRequest } from "./mfa/types.js";
17
20
  export type { BlocksMailPassThroughResponse, BlocksSendMailRequest, BlocksSendMailToAnyRequest } from "./mail/types.js";
@@ -58,6 +58,8 @@ export declare class BlocksMfaClient {
58
58
  * What: sets the signed-in user's active MFA method through `PUT /iam/v4/mfa/method`.
59
59
  * Why: users who have enrolled more than one method need to switch which one is active.
60
60
  * How: pass the `mfaType` to activate; enroll it first (e.g. via `totp.setup`/`verifySetup`) if it isn't already.
61
+ * Careful: IAM only branches on TOTP (1) and Email (2) here -- every other value falls through to its
62
+ * disable path and turns the user's MFA off. Use `disable()` when that is the intent.
61
63
  */
62
64
  setMethod(request: BlocksMfaSetMethodRequest): Promise<BlocksMfaPassThroughResponse>;
63
65
  /**
@@ -69,6 +69,8 @@ export class BlocksMfaClient {
69
69
  * What: sets the signed-in user's active MFA method through `PUT /iam/v4/mfa/method`.
70
70
  * Why: users who have enrolled more than one method need to switch which one is active.
71
71
  * How: pass the `mfaType` to activate; enroll it first (e.g. via `totp.setup`/`verifySetup`) if it isn't already.
72
+ * Careful: IAM only branches on TOTP (1) and Email (2) here -- every other value falls through to its
73
+ * disable path and turns the user's MFA off. Use `disable()` when that is the intent.
72
74
  */
73
75
  setMethod(request) {
74
76
  return this.http.request(`${MFA_API}/method`, {
@@ -21,7 +21,7 @@ export type BlocksMfaConfigSaveRequest = Record<string, unknown> & {
21
21
  userMfaType?: number[];
22
22
  };
23
23
  export type BlocksMfaGenerateRequest = {
24
- /** IAM-defined numeric MFA method (its enum names aren't in the swagger contract -- treat as opaque). */
24
+ /** IAM's UserMfaType: 0 None, 1 TOTP, 2 Email, 3 Sms, 4 WhatsApp. Only 1 and 2 have a provider. */
25
25
  mfaType: number;
26
26
  sendPhoneNumberAsEmailDomain?: string;
27
27
  };
@@ -30,6 +30,7 @@ export type BlocksMfaResendRequest = {
30
30
  sendPhoneNumberAsEmailDomain?: string;
31
31
  };
32
32
  export type BlocksMfaVerifyRequest = {
33
+ /** Same UserMfaType enum as `mfaType`: the method that issued this challenge (1 TOTP, 2 Email). */
33
34
  authType: number;
34
35
  isFromTokenCall?: boolean;
35
36
  mfaId: string;
@@ -16,7 +16,7 @@ export type BlocksNotifyRequest = {
16
16
  userIds?: string[];
17
17
  };
18
18
  export type BlocksGetUnreadNotificationsBySubscriptionFilterRequest = {
19
- /** IAM/Notifier-defined order enum (its meanings aren't in the swagger contract -- treat as opaque). */
19
+ /** 1 = CreatedTime (newest first), 2 = ReadStatus. Any other value (including an omitted 0) returns an empty list. */
20
20
  orderBy?: 1 | 2;
21
21
  subscriptionFilterData?: BlocksNotifierSubscriptionFilter;
22
22
  userId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seliseblocks/client",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-neutral TypeScript client SDK for SELISE Blocks frontend applications.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,6 +11,16 @@
11
11
  "typescript",
12
12
  "frontend"
13
13
  ],
14
+ "homepage": "https://github.com/SELISEdigitalplatforms/blocks-cli/tree/main/blocks-client#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/SELISEdigitalplatforms/blocks-cli/issues"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/SELISEdigitalplatforms/blocks-cli.git",
21
+ "directory": "blocks-client"
22
+ },
23
+ "author": "SELISE Digital Platforms",
14
24
  "main": "./dist/index.js",
15
25
  "types": "./dist/index.d.ts",
16
26
  "exports": {
@@ -27,7 +37,8 @@
27
37
  "files": [
28
38
  "dist",
29
39
  "README.md",
30
- "AI_USAGE_GUIDE.md",
40
+ "CHANGELOG.md",
41
+ "AGENT_GUIDE.md",
31
42
  "LICENSE"
32
43
  ],
33
44
  "scripts": {