@blazeo.com/calendar-client 1.0.24 → 1.0.26

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.
package/README.md CHANGED
@@ -22,12 +22,26 @@ cd your-app && npm link @blazeo.com/calendar-client
22
22
  Configure once at app startup, then use models and their methods:
23
23
 
24
24
  ```js
25
- import { configure, setBaseUrl, setConsumer, CalendarModel, createRootStore } from '@blazeo.com/calendar-client';
25
+ import {
26
+ configure,
27
+ setBaseUrl,
28
+ setConsumer,
29
+ setApiCredentials,
30
+ fetchAccessToken,
31
+ CalendarModel,
32
+ createRootStore,
33
+ } from '@blazeo.com/calendar-client';
26
34
 
27
35
  configure({ baseUrl: 'https://your-appointment-api.example.com' });
28
36
  // or: setBaseUrl('https://localhost:7051');
29
37
  setConsumer('my-app'); // optional: sent as Consumer header (e.g. for lead source)
30
38
 
39
+ // API JWT (for Authorized routes such as lead export)
40
+ setApiCredentials('your-api-key', 'your-api-secret');
41
+ await fetchAccessToken();
42
+ // Or set a token you obtained elsewhere:
43
+ // setAccessToken('eyJ...', '2026-05-19T12:00:00Z');
44
+
31
45
  // Calendar static methods (no store needed)
32
46
  const timezones = await CalendarModel.getTimeZones();
33
47
  const calendar = await CalendarModel.get('calendar-guid');
@@ -38,12 +52,33 @@ const cal = store.addCalendar({ calendarId: 'my-cal', name: 'My Calendar' });
38
52
  await cal.create(); // POST to backend
39
53
  ```
40
54
 
55
+ ### API JWT authentication
56
+
57
+ The backend issues JWTs via `POST Api/Auth/Token` (`api_key` + `api_secret`). There is no refresh endpoint; when the token expires (~1 hour), call `fetchAccessToken()` again.
58
+
59
+ | Function | Purpose |
60
+ |----------|---------|
61
+ | `setApiCredentials(apiKey, apiSecret)` | Store credentials for token exchange |
62
+ | `fetchAccessToken()` | Get JWT from API and store on config |
63
+ | `setAccessToken(token, expiresAtUtc?)` | Use a token you already have |
64
+ | `ensureValidAccessToken()` | Fetch only if missing or near expiry |
65
+ | `clearAuth()` | Clear token and credentials |
66
+
67
+ All `reqGet` / `reqPost` calls automatically attach `Authorization: Bearer …` and re-issue the token before requests when credentials are configured.
68
+
69
+ ```js
70
+ configure({ baseUrl: 'https://your-api', apiKey: 'key', apiSecret: 'secret' });
71
+ await fetchAccessToken();
72
+ await LeadModel.requestExport('company-key');
73
+ ```
74
+
41
75
  ## API overview
42
76
 
43
77
  - **CalendarModel (static):** `get`, `getByCompany`, `getTimeZones`, `getTimeZone`, `getParticipants`, `getMonth`, `getEvents`, etc.
44
78
  - **EventModel (instance):** `get`, `create`, `cancel`, `getCancellable`, `getAvailability`, `setReminder`
45
79
  - **FlowModel:** Same pattern as Calendar — `FlowModel.create({}, { env })` with no fields; static `get`, `getRaw`, `list`, `createFlow`, `updateFlow`, `delete`, `duplicate`, appearance/embed/public/preview helpers; instance methods mirror those using `flowId` on the snapshot
46
80
  - **LeadModel:** `LeadModel.create({}, { env })`; static `get`, `getRaw`, `getByEmail`, `getByCompany`; instance `get`, `getByEmail`, `getByCompany` (uses `leadId` / `email` / `companyKey` on the snapshot)
81
+ - **ParticipantModel:** static `get`, `getByEmail`, `getByIds`, `getAll`, …; instance `getByEmail` uses `email` + `companyKey` on the snapshot (see `GET /participant/getbyemail`)
47
82
  - **AuthModel (calendar OAuth / Connect Calendar):** `getCalendarProviders`, `getAuthorizationUrl`, `getAuthorizationStatus`, `openOAuthPopup`, `onCalendarAuthMessage` — see [Calendar authorization flow](#calendar-authorization-direct-ui)
48
83
  - **RootStore:** `addCalendar`, `addEvent`
49
84
 
package/dist/index.d.ts CHANGED
@@ -1,9 +1,59 @@
1
1
  /** @blazeo.com/calendar-client - type declarations */
2
2
 
3
- export function configure(env: { baseUrl?: string; consumer?: string; fetch?: typeof fetch; getDefaultOffset?: () => number }): void;
4
- export function getConfig(): { baseUrl?: string; consumer?: string; fetch?: typeof fetch; getDefaultOffset?: () => number } | null;
3
+ export type AccessTokenResult = {
4
+ status: string;
5
+ message?: string;
6
+ accessToken?: string;
7
+ expiresAtUtc?: string | null;
8
+ tokenType?: string;
9
+ };
10
+
11
+ export type AuthState = {
12
+ accessToken?: string;
13
+ tokenExpiresAt?: string;
14
+ hasApiCredentials: boolean;
15
+ };
16
+
17
+ export function configure(env: {
18
+ baseUrl?: string;
19
+ consumer?: string;
20
+ fetch?: typeof fetch;
21
+ getDefaultOffset?: () => number;
22
+ accessToken?: string;
23
+ expiresAtUtc?: string;
24
+ tokenExpiresAt?: string;
25
+ expires_at_utc?: string;
26
+ apiKey?: string;
27
+ api_key?: string;
28
+ apiSecret?: string;
29
+ api_secret?: string;
30
+ }): void;
31
+
32
+ export function getConfig(): {
33
+ baseUrl?: string;
34
+ consumer?: string;
35
+ fetch?: typeof fetch;
36
+ getDefaultOffset?: () => number;
37
+ accessToken?: string;
38
+ tokenExpiresAt?: string;
39
+ hasApiCredentials?: boolean;
40
+ } | null;
41
+
5
42
  export function setBaseUrl(baseUrl: string): void;
6
43
  export function setConsumer(consumer: string): void;
44
+ export function setAccessToken(accessToken: string, expiresAtUtc?: string): void;
45
+ export function clearAccessToken(): void;
46
+ export function setApiCredentials(apiKey: string, apiSecret: string): void;
47
+ export function clearApiCredentials(): void;
48
+ export function clearAuth(): void;
49
+ export function getAuth(): AuthState;
50
+ export function fetchAccessToken(apiKey?: string, apiSecret?: string): Promise<AccessTokenResult>;
51
+ export function ensureValidAccessToken(): Promise<string | undefined>;
52
+ export function requestAccessToken(apiKey: string, apiSecret: string, opts?: { baseUrl?: string; fetch?: typeof fetch }): Promise<AccessTokenResult>;
53
+ export function isAccessTokenExpired(expiresAtUtc?: string | number | Date | null, skewMs?: number): boolean;
54
+ export function buildAuthHeaders(extra?: Record<string, string>): Record<string, string>;
55
+ export const TOKEN_PATH: '/Api/Auth/Token';
56
+ export const DEFAULT_TOKEN_REFRESH_SKEW_MS: number;
7
57
  export function getConfigStore(): unknown;
8
58
 
9
59
  export const ConfigModel: unknown;
@@ -79,6 +129,7 @@ export const CalendarParticipantModel: {
79
129
  };
80
130
  export const ParticipantModel: {
81
131
  get(participantId: string): Promise<unknown>;
132
+ getByEmail(email: string, companyKey: string): Promise<unknown>;
82
133
  getByIds(participantIds: string[] | string): Promise<unknown[] | null>;
83
134
  getAll(companyKey: string): Promise<unknown[] | null>;
84
135
  add(payload: object, calendarId?: string): Promise<unknown>;
package/dist/index.js CHANGED
@@ -48,14 +48,27 @@ __export(index_exports, {
48
48
  RecurringFrequency: () => RecurringFrequency,
49
49
  RootStore: () => RootStore,
50
50
  SettingModel: () => Setting_default,
51
+ TOKEN_PATH: () => TOKEN_PATH,
51
52
  TimeFrameModel: () => TimeFrame_default,
52
53
  TimeSlotModel: () => TimeSlot_default,
53
54
  Unit: () => Unit,
55
+ buildAuthHeaders: () => buildAuthHeaders,
56
+ clearAccessToken: () => clearAccessToken,
57
+ clearApiCredentials: () => clearApiCredentials,
58
+ clearAuth: () => clearAuth,
54
59
  configure: () => configure,
55
60
  createRootStore: () => createRootStore,
61
+ ensureValidAccessToken: () => ensureValidAccessToken,
62
+ fetchAccessToken: () => fetchAccessToken2,
63
+ getAuth: () => getAuth,
56
64
  getConfig: () => getConfig,
57
65
  getConfigStore: () => getConfigStore,
58
- setBaseUrl: () => setBaseUrl
66
+ isAccessTokenExpired: () => isAccessTokenExpired,
67
+ requestAccessToken: () => requestAccessToken,
68
+ setAccessToken: () => setAccessToken,
69
+ setApiCredentials: () => setApiCredentials,
70
+ setBaseUrl: () => setBaseUrl,
71
+ setConsumer: () => setConsumer
59
72
  });
60
73
  module.exports = __toCommonJS(index_exports);
61
74
 
@@ -69,6 +82,10 @@ var ConfigModel = import_mobx_state_tree.types.model("Config", {
69
82
  consumer: import_mobx_state_tree.types.optional(import_mobx_state_tree.types.string, "")
70
83
  }).volatile(() => ({
71
84
  fetch: void 0,
85
+ accessToken: void 0,
86
+ tokenExpiresAt: void 0,
87
+ apiKey: void 0,
88
+ apiSecret: void 0,
72
89
  getDefaultOffset: () => -(/* @__PURE__ */ new Date()).getTimezoneOffset()
73
90
  })).actions((self) => ({
74
91
  setBaseUrl(url) {
@@ -83,19 +100,57 @@ var ConfigModel = import_mobx_state_tree.types.model("Config", {
83
100
  setGetDefaultOffset(fn) {
84
101
  self.getDefaultOffset = fn;
85
102
  },
103
+ setAccessToken(token, expiresAtUtc = void 0) {
104
+ self.accessToken = token ? String(token) : void 0;
105
+ self.tokenExpiresAt = expiresAtUtc != null && expiresAtUtc !== "" ? String(expiresAtUtc) : void 0;
106
+ },
107
+ clearAccessToken() {
108
+ self.accessToken = void 0;
109
+ self.tokenExpiresAt = void 0;
110
+ },
111
+ setApiCredentials(apiKey, apiSecret) {
112
+ self.apiKey = apiKey != null ? String(apiKey) : void 0;
113
+ self.apiSecret = apiSecret != null ? String(apiSecret) : void 0;
114
+ },
115
+ clearApiCredentials() {
116
+ self.apiKey = void 0;
117
+ self.apiSecret = void 0;
118
+ },
119
+ clearAuth() {
120
+ self.clearAccessToken();
121
+ self.clearApiCredentials();
122
+ },
86
123
  configure(env) {
87
124
  if (env.baseUrl != null) self.baseUrl = env.baseUrl;
88
125
  if (env.consumer != null) self.consumer = env.consumer;
89
126
  if (env.fetch != null) self.fetch = env.fetch;
90
127
  if (env.getDefaultOffset != null) self.getDefaultOffset = env.getDefaultOffset;
128
+ if (env.accessToken != null) {
129
+ self.setAccessToken(
130
+ env.accessToken,
131
+ env.expiresAtUtc ?? env.tokenExpiresAt ?? env.expires_at_utc
132
+ );
133
+ }
134
+ if (env.apiKey != null || env.api_key != null) {
135
+ self.apiKey = String(env.apiKey ?? env.api_key);
136
+ }
137
+ if (env.apiSecret != null || env.api_secret != null) {
138
+ self.apiSecret = String(env.apiSecret ?? env.api_secret);
139
+ }
91
140
  }
92
141
  })).views((self) => ({
142
+ get hasApiCredentials() {
143
+ return Boolean(self.apiKey && self.apiSecret);
144
+ },
93
145
  getEnv() {
94
146
  return {
95
147
  baseUrl: self.baseUrl || void 0,
96
148
  consumer: self.consumer || void 0,
97
149
  fetch: self.fetch,
98
- getDefaultOffset: self.getDefaultOffset
150
+ getDefaultOffset: self.getDefaultOffset,
151
+ accessToken: self.accessToken,
152
+ tokenExpiresAt: self.tokenExpiresAt,
153
+ hasApiCredentials: self.hasApiCredentials
99
154
  };
100
155
  }
101
156
  }));
@@ -106,6 +161,122 @@ function getConfigStore() {
106
161
  }
107
162
  var ConfigModel_default = ConfigModel;
108
163
 
164
+ // src/apiAuth.js
165
+ var TOKEN_PATH = "/Api/Auth/Token";
166
+ var DEFAULT_TOKEN_REFRESH_SKEW_MS = 6e4;
167
+ function defaultFetch() {
168
+ if (typeof fetch === "undefined") {
169
+ throw new Error("fetch not available");
170
+ }
171
+ return fetch;
172
+ }
173
+ function pickTokenPayload(data) {
174
+ if (!data || typeof data !== "object") return null;
175
+ const nested = data.data && typeof data.data === "object" ? data.data : data;
176
+ const accessToken = nested.access_token ?? nested.accessToken ?? nested.AccessToken ?? null;
177
+ if (!accessToken) return null;
178
+ const expiresAtUtc = nested.expires_at_utc ?? nested.expiresAtUtc ?? nested.ExpiresAtUtc ?? null;
179
+ return {
180
+ accessToken: String(accessToken),
181
+ expiresAtUtc: expiresAtUtc != null ? String(expiresAtUtc) : null,
182
+ tokenType: nested.token_type ?? nested.tokenType ?? "Bearer"
183
+ };
184
+ }
185
+ function isAccessTokenExpired(expiresAtUtc, skewMs = DEFAULT_TOKEN_REFRESH_SKEW_MS) {
186
+ if (expiresAtUtc == null || expiresAtUtc === "") return false;
187
+ const expMs = new Date(expiresAtUtc).getTime();
188
+ if (Number.isNaN(expMs)) return false;
189
+ return Date.now() >= expMs - skewMs;
190
+ }
191
+ function getAuthState() {
192
+ const store = getConfigStore();
193
+ return {
194
+ accessToken: store.accessToken ?? void 0,
195
+ tokenExpiresAt: store.tokenExpiresAt ?? void 0,
196
+ apiKey: store.apiKey ?? void 0,
197
+ apiSecret: store.apiSecret ?? void 0,
198
+ hasApiCredentials: Boolean(store.apiKey && store.apiSecret)
199
+ };
200
+ }
201
+ async function requestAccessToken(apiKey, apiSecret, opts = {}) {
202
+ const store = getConfigStore();
203
+ const baseUrl = opts.baseUrl ?? store.baseUrl;
204
+ if (!baseUrl) {
205
+ return { status: "failure", message: "baseUrl required. Call configure({ baseUrl }) first." };
206
+ }
207
+ const key = apiKey != null ? String(apiKey).trim() : "";
208
+ const secret = apiSecret != null ? String(apiSecret).trim() : "";
209
+ if (!key || !secret) {
210
+ return { status: "failure", message: "api_key and api_secret are required" };
211
+ }
212
+ const fetchFn = opts.fetch ?? store.fetch ?? defaultFetch();
213
+ const url = `${String(baseUrl).replace(/\/+$/, "")}${TOKEN_PATH}`;
214
+ const httpRes = await fetchFn(url, {
215
+ method: "POST",
216
+ headers: { "Content-Type": "application/json" },
217
+ body: JSON.stringify({ api_key: key, api_secret: secret })
218
+ });
219
+ const text = await httpRes.text();
220
+ let body;
221
+ try {
222
+ body = JSON.parse(text);
223
+ } catch {
224
+ body = { status: "failure", message: text || httpRes.statusText };
225
+ }
226
+ if (!httpRes.ok && body.status !== "failure") {
227
+ body.status = "failure";
228
+ body.message = body.message ?? `HTTP ${httpRes.status}`;
229
+ }
230
+ if (body.status !== "success") {
231
+ return {
232
+ status: "failure",
233
+ message: body.message ?? "Failed to obtain access token"
234
+ };
235
+ }
236
+ const parsed = pickTokenPayload(body);
237
+ if (!parsed) {
238
+ return { status: "failure", message: "Token response missing access_token" };
239
+ }
240
+ return {
241
+ status: "success",
242
+ accessToken: parsed.accessToken,
243
+ expiresAtUtc: parsed.expiresAtUtc,
244
+ tokenType: parsed.tokenType
245
+ };
246
+ }
247
+ async function fetchAccessToken(apiKey, apiSecret) {
248
+ const store = getConfigStore();
249
+ const key = apiKey ?? store.apiKey;
250
+ const secret = apiSecret ?? store.apiSecret;
251
+ const result = await requestAccessToken(key, secret);
252
+ if (result.status === "success") {
253
+ store.setAccessToken(result.accessToken, result.expiresAtUtc);
254
+ }
255
+ return result;
256
+ }
257
+ async function ensureAccessToken() {
258
+ const store = getConfigStore();
259
+ const { accessToken, tokenExpiresAt, apiKey, apiSecret } = getAuthState();
260
+ if (accessToken && !isAccessTokenExpired(tokenExpiresAt)) {
261
+ return accessToken;
262
+ }
263
+ if (apiKey && apiSecret) {
264
+ const result = await fetchAccessToken(apiKey, apiSecret);
265
+ if (result.status === "success") return result.accessToken;
266
+ }
267
+ return accessToken;
268
+ }
269
+ function buildAuthHeaders(extra = {}) {
270
+ const headers = { ...extra };
271
+ const { consumer } = getConfigStore();
272
+ const { accessToken } = getAuthState();
273
+ if (!headers.Consumer && consumer) headers.Consumer = consumer;
274
+ if (!headers.Authorization && accessToken) {
275
+ headers.Authorization = `Bearer ${accessToken}`;
276
+ }
277
+ return headers;
278
+ }
279
+
109
280
  // src/config.js
110
281
  function configure(env) {
111
282
  const store = getConfigStore();
@@ -119,6 +290,33 @@ function getConfig() {
119
290
  function setBaseUrl(baseUrl) {
120
291
  getConfigStore().setBaseUrl(baseUrl);
121
292
  }
293
+ function setConsumer(consumer) {
294
+ getConfigStore().setConsumer(consumer);
295
+ }
296
+ function setAccessToken(accessToken, expiresAtUtc) {
297
+ getConfigStore().setAccessToken(accessToken, expiresAtUtc);
298
+ }
299
+ function clearAccessToken() {
300
+ getConfigStore().clearAccessToken();
301
+ }
302
+ function setApiCredentials(apiKey, apiSecret) {
303
+ getConfigStore().setApiCredentials(apiKey, apiSecret);
304
+ }
305
+ function clearApiCredentials() {
306
+ getConfigStore().clearApiCredentials();
307
+ }
308
+ function clearAuth() {
309
+ getConfigStore().clearAuth();
310
+ }
311
+ function getAuth() {
312
+ return getAuthState();
313
+ }
314
+ function fetchAccessToken2(apiKey, apiSecret) {
315
+ return fetchAccessToken(apiKey, apiSecret);
316
+ }
317
+ function ensureValidAccessToken() {
318
+ return ensureAccessToken();
319
+ }
122
320
 
123
321
  // src/apiRequest.js
124
322
  function buildQuery(params) {
@@ -135,10 +333,18 @@ function buildQuery(params) {
135
333
  return q ? `?${q}` : "";
136
334
  }
137
335
  async function request(baseUrl, fetchFn, path, options = {}) {
138
- const { method = "GET", headers = {}, body, query, skipContentType } = options;
336
+ const { method = "GET", headers = {}, body, query, skipContentType, skipAuth } = options;
139
337
  const url = `${String(baseUrl).replace(/\/+$/, "")}${path}${buildQuery(query)}`;
140
338
  const reqHeaders = { ...headers };
141
339
  if (!skipContentType && typeof body === "string") reqHeaders["Content-Type"] = "application/json";
340
+ if (!skipAuth) {
341
+ const store = getConfigStore();
342
+ if (!reqHeaders["Consumer"] && store.consumer) reqHeaders["Consumer"] = store.consumer;
343
+ const { accessToken } = getAuthState();
344
+ if (!reqHeaders["Authorization"] && accessToken) {
345
+ reqHeaders["Authorization"] = `Bearer ${accessToken}`;
346
+ }
347
+ }
142
348
  const res = await fetchFn(url, { method, headers: reqHeaders, body });
143
349
  const text = await res.text();
144
350
  let data;
@@ -151,23 +357,35 @@ async function request(baseUrl, fetchFn, path, options = {}) {
151
357
  data.status = "failure";
152
358
  data.message = data.message ?? `HTTP ${res.status}`;
153
359
  }
360
+ data._httpStatus = res.status;
154
361
  return data;
155
362
  }
156
- function mergeConsumerHeader(env, opts) {
363
+ function mergeRequestHeaders(env, opts) {
157
364
  const headers = { ...opts.headers || {} };
158
- if (!headers["Consumer"] && (env == null ? void 0 : env.consumer)) headers["Consumer"] = env.consumer;
365
+ const store = getConfigStore();
366
+ if (!headers["Consumer"] && ((env == null ? void 0 : env.consumer) || store.consumer)) {
367
+ headers["Consumer"] = (env == null ? void 0 : env.consumer) || store.consumer;
368
+ }
369
+ const { accessToken } = getAuthState();
370
+ if (!headers["Authorization"] && accessToken) {
371
+ headers["Authorization"] = `Bearer ${accessToken}`;
372
+ }
159
373
  return { ...opts, headers };
160
374
  }
375
+ async function executeRequest(baseUrl, fetchFn, path, opts) {
376
+ if (!opts.skipAuth) await ensureAccessToken();
377
+ return request(baseUrl, fetchFn, path, mergeRequestHeaders(null, opts));
378
+ }
161
379
  function createRequestHelpers(self, getEnv12) {
162
380
  const env = () => getEnv12(self);
163
381
  const baseUrl = () => env().baseUrl;
164
- const fetchFn = () => env().fetch ?? (typeof fetch !== "undefined" ? fetch : () => {
382
+ const fetchFn = () => env().fetch ?? getConfigStore().fetch ?? (typeof fetch !== "undefined" ? fetch : () => {
165
383
  throw new Error("fetch not available");
166
384
  });
167
385
  const req = (path, opts = {}) => {
168
386
  const url = baseUrl();
169
387
  if (!url) throw new Error("Model env requires baseUrl. Call configure({ baseUrl }) at app startup.");
170
- return request(url, fetchFn(), path, mergeConsumerHeader(env(), opts));
388
+ return executeRequest(url, fetchFn(), path, opts);
171
389
  };
172
390
  return {
173
391
  req,
@@ -178,14 +396,14 @@ function createRequestHelpers(self, getEnv12) {
178
396
  function createRequestHelpersFromEnv(env) {
179
397
  const e = env ?? getConfig();
180
398
  if (!e) throw new Error("Env required. Pass env to the method or call configure({ baseUrl }) at app startup.");
181
- const baseUrl = () => e == null ? void 0 : e.baseUrl;
182
- const fetchFn = () => (e == null ? void 0 : e.fetch) ?? (typeof fetch !== "undefined" ? fetch : () => {
399
+ const baseUrl = () => (e == null ? void 0 : e.baseUrl) ?? getConfigStore().baseUrl;
400
+ const fetchFn = () => (e == null ? void 0 : e.fetch) ?? getConfigStore().fetch ?? (typeof fetch !== "undefined" ? fetch : () => {
183
401
  throw new Error("fetch not available");
184
402
  });
185
403
  const req = (path, opts = {}) => {
186
404
  const url = baseUrl();
187
405
  if (!url) throw new Error("Env requires baseUrl. Call configure({ baseUrl }) at app startup.");
188
- return request(url, fetchFn(), path, mergeConsumerHeader(e, opts));
406
+ return executeRequest(url, fetchFn(), path, opts);
189
407
  };
190
408
  return {
191
409
  env: e,
@@ -1300,6 +1518,15 @@ var ParticipantModel = import_mobx_state_tree11.types.model("Participant", {
1300
1518
  if (res.status === "success" && res.data) (0, import_mobx_state_tree11.applySnapshot)(self, mapFromApi2(res.data));
1301
1519
  return res;
1302
1520
  },
1521
+ /** GET participant/getbyemail – fetch by companyKey + email on this snapshot */
1522
+ async getByEmail() {
1523
+ const res = await reqGet("/participant/getbyemail", {
1524
+ email: self.email,
1525
+ company_key: self.companyKey
1526
+ });
1527
+ if (res.status === "success" && res.data) (0, import_mobx_state_tree11.applySnapshot)(self, mapFromApi2(res.data));
1528
+ return res;
1529
+ },
1303
1530
  /** POST participant/save – save participant (add or update) */
1304
1531
  async save() {
1305
1532
  const payload = toPayload(self);
@@ -1415,6 +1642,14 @@ function mapCalendarFromApi2(d) {
1415
1642
  modifiedOn: pick2("modifiedOn", "ModifiedOn", "modified_on") ?? null
1416
1643
  };
1417
1644
  }
1645
+ ParticipantModel.getByEmail = async (email, companyKey) => {
1646
+ const { reqGet } = createRequestHelpersFromEnv(getConfig());
1647
+ const res = await reqGet("/participant/getbyemail", { email, company_key: companyKey });
1648
+ if (res.status === "success" && res.data) {
1649
+ return ParticipantModel.create(mapFromApi2(res.data), { env: getConfig() });
1650
+ }
1651
+ return null;
1652
+ };
1418
1653
  ParticipantModel.get = async (participantId) => {
1419
1654
  const { reqGet } = createRequestHelpersFromEnv(getConfig());
1420
1655
  const res = await reqGet("/participant/get", { participant_id: participantId });
@@ -1593,6 +1828,7 @@ SettingModel.save = async (payload) => {
1593
1828
  SettingModel.uploadLogo = async (calendarId, file) => {
1594
1829
  const cfg = getConfig();
1595
1830
  if (!(cfg == null ? void 0 : cfg.baseUrl)) throw new Error("Configure baseUrl before uploadLogo");
1831
+ await ensureAccessToken();
1596
1832
  const fetchFn = cfg.fetch ?? (typeof fetch !== "undefined" ? fetch : () => {
1597
1833
  throw new Error("fetch not available");
1598
1834
  });
@@ -1602,6 +1838,7 @@ SettingModel.uploadLogo = async (calendarId, file) => {
1602
1838
  formData.append("file", file);
1603
1839
  const res = await fetchFn(url, {
1604
1840
  method: "POST",
1841
+ headers: buildAuthHeaders(),
1605
1842
  body: formData
1606
1843
  });
1607
1844
  const text = await res.text();
@@ -1724,6 +1961,7 @@ AssetModel.upload = async (file, opts = {}) => {
1724
1961
  if (!file) return { status: "failure", message: "file is required" };
1725
1962
  const category = opts.category != null ? String(opts.category).trim() : "";
1726
1963
  if (!category) return { status: "failure", message: "category is required" };
1964
+ await ensureAccessToken();
1727
1965
  const fetchFn = cfg.fetch ?? (typeof fetch !== "undefined" ? fetch : () => {
1728
1966
  throw new Error("fetch not available");
1729
1967
  });
@@ -1738,8 +1976,7 @@ AssetModel.upload = async (file, opts = {}) => {
1738
1976
  if (opts.calendarId != null && String(opts.calendarId).trim() !== "") {
1739
1977
  formData.append("calendar_id", String(opts.calendarId).trim());
1740
1978
  }
1741
- const headers = {};
1742
- if (cfg.consumer) headers.Consumer = cfg.consumer;
1979
+ const headers = buildAuthHeaders();
1743
1980
  if (opts.consumer != null && String(opts.consumer).trim() !== "") {
1744
1981
  headers.Consumer = String(opts.consumer).trim();
1745
1982
  }
@@ -2796,12 +3033,25 @@ function createRootStore(initialState = {}) {
2796
3033
  RecurringFrequency,
2797
3034
  RootStore,
2798
3035
  SettingModel,
3036
+ TOKEN_PATH,
2799
3037
  TimeFrameModel,
2800
3038
  TimeSlotModel,
2801
3039
  Unit,
3040
+ buildAuthHeaders,
3041
+ clearAccessToken,
3042
+ clearApiCredentials,
3043
+ clearAuth,
2802
3044
  configure,
2803
3045
  createRootStore,
3046
+ ensureValidAccessToken,
3047
+ fetchAccessToken,
3048
+ getAuth,
2804
3049
  getConfig,
2805
3050
  getConfigStore,
2806
- setBaseUrl
3051
+ isAccessTokenExpired,
3052
+ requestAccessToken,
3053
+ setAccessToken,
3054
+ setApiCredentials,
3055
+ setBaseUrl,
3056
+ setConsumer
2807
3057
  });
package/dist/index.mjs CHANGED
@@ -8,6 +8,10 @@ var ConfigModel = types.model("Config", {
8
8
  consumer: types.optional(types.string, "")
9
9
  }).volatile(() => ({
10
10
  fetch: void 0,
11
+ accessToken: void 0,
12
+ tokenExpiresAt: void 0,
13
+ apiKey: void 0,
14
+ apiSecret: void 0,
11
15
  getDefaultOffset: () => -(/* @__PURE__ */ new Date()).getTimezoneOffset()
12
16
  })).actions((self) => ({
13
17
  setBaseUrl(url) {
@@ -22,19 +26,57 @@ var ConfigModel = types.model("Config", {
22
26
  setGetDefaultOffset(fn) {
23
27
  self.getDefaultOffset = fn;
24
28
  },
29
+ setAccessToken(token, expiresAtUtc = void 0) {
30
+ self.accessToken = token ? String(token) : void 0;
31
+ self.tokenExpiresAt = expiresAtUtc != null && expiresAtUtc !== "" ? String(expiresAtUtc) : void 0;
32
+ },
33
+ clearAccessToken() {
34
+ self.accessToken = void 0;
35
+ self.tokenExpiresAt = void 0;
36
+ },
37
+ setApiCredentials(apiKey, apiSecret) {
38
+ self.apiKey = apiKey != null ? String(apiKey) : void 0;
39
+ self.apiSecret = apiSecret != null ? String(apiSecret) : void 0;
40
+ },
41
+ clearApiCredentials() {
42
+ self.apiKey = void 0;
43
+ self.apiSecret = void 0;
44
+ },
45
+ clearAuth() {
46
+ self.clearAccessToken();
47
+ self.clearApiCredentials();
48
+ },
25
49
  configure(env) {
26
50
  if (env.baseUrl != null) self.baseUrl = env.baseUrl;
27
51
  if (env.consumer != null) self.consumer = env.consumer;
28
52
  if (env.fetch != null) self.fetch = env.fetch;
29
53
  if (env.getDefaultOffset != null) self.getDefaultOffset = env.getDefaultOffset;
54
+ if (env.accessToken != null) {
55
+ self.setAccessToken(
56
+ env.accessToken,
57
+ env.expiresAtUtc ?? env.tokenExpiresAt ?? env.expires_at_utc
58
+ );
59
+ }
60
+ if (env.apiKey != null || env.api_key != null) {
61
+ self.apiKey = String(env.apiKey ?? env.api_key);
62
+ }
63
+ if (env.apiSecret != null || env.api_secret != null) {
64
+ self.apiSecret = String(env.apiSecret ?? env.api_secret);
65
+ }
30
66
  }
31
67
  })).views((self) => ({
68
+ get hasApiCredentials() {
69
+ return Boolean(self.apiKey && self.apiSecret);
70
+ },
32
71
  getEnv() {
33
72
  return {
34
73
  baseUrl: self.baseUrl || void 0,
35
74
  consumer: self.consumer || void 0,
36
75
  fetch: self.fetch,
37
- getDefaultOffset: self.getDefaultOffset
76
+ getDefaultOffset: self.getDefaultOffset,
77
+ accessToken: self.accessToken,
78
+ tokenExpiresAt: self.tokenExpiresAt,
79
+ hasApiCredentials: self.hasApiCredentials
38
80
  };
39
81
  }
40
82
  }));
@@ -45,6 +87,122 @@ function getConfigStore() {
45
87
  }
46
88
  var ConfigModel_default = ConfigModel;
47
89
 
90
+ // src/apiAuth.js
91
+ var TOKEN_PATH = "/Api/Auth/Token";
92
+ var DEFAULT_TOKEN_REFRESH_SKEW_MS = 6e4;
93
+ function defaultFetch() {
94
+ if (typeof fetch === "undefined") {
95
+ throw new Error("fetch not available");
96
+ }
97
+ return fetch;
98
+ }
99
+ function pickTokenPayload(data) {
100
+ if (!data || typeof data !== "object") return null;
101
+ const nested = data.data && typeof data.data === "object" ? data.data : data;
102
+ const accessToken = nested.access_token ?? nested.accessToken ?? nested.AccessToken ?? null;
103
+ if (!accessToken) return null;
104
+ const expiresAtUtc = nested.expires_at_utc ?? nested.expiresAtUtc ?? nested.ExpiresAtUtc ?? null;
105
+ return {
106
+ accessToken: String(accessToken),
107
+ expiresAtUtc: expiresAtUtc != null ? String(expiresAtUtc) : null,
108
+ tokenType: nested.token_type ?? nested.tokenType ?? "Bearer"
109
+ };
110
+ }
111
+ function isAccessTokenExpired(expiresAtUtc, skewMs = DEFAULT_TOKEN_REFRESH_SKEW_MS) {
112
+ if (expiresAtUtc == null || expiresAtUtc === "") return false;
113
+ const expMs = new Date(expiresAtUtc).getTime();
114
+ if (Number.isNaN(expMs)) return false;
115
+ return Date.now() >= expMs - skewMs;
116
+ }
117
+ function getAuthState() {
118
+ const store = getConfigStore();
119
+ return {
120
+ accessToken: store.accessToken ?? void 0,
121
+ tokenExpiresAt: store.tokenExpiresAt ?? void 0,
122
+ apiKey: store.apiKey ?? void 0,
123
+ apiSecret: store.apiSecret ?? void 0,
124
+ hasApiCredentials: Boolean(store.apiKey && store.apiSecret)
125
+ };
126
+ }
127
+ async function requestAccessToken(apiKey, apiSecret, opts = {}) {
128
+ const store = getConfigStore();
129
+ const baseUrl = opts.baseUrl ?? store.baseUrl;
130
+ if (!baseUrl) {
131
+ return { status: "failure", message: "baseUrl required. Call configure({ baseUrl }) first." };
132
+ }
133
+ const key = apiKey != null ? String(apiKey).trim() : "";
134
+ const secret = apiSecret != null ? String(apiSecret).trim() : "";
135
+ if (!key || !secret) {
136
+ return { status: "failure", message: "api_key and api_secret are required" };
137
+ }
138
+ const fetchFn = opts.fetch ?? store.fetch ?? defaultFetch();
139
+ const url = `${String(baseUrl).replace(/\/+$/, "")}${TOKEN_PATH}`;
140
+ const httpRes = await fetchFn(url, {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({ api_key: key, api_secret: secret })
144
+ });
145
+ const text = await httpRes.text();
146
+ let body;
147
+ try {
148
+ body = JSON.parse(text);
149
+ } catch {
150
+ body = { status: "failure", message: text || httpRes.statusText };
151
+ }
152
+ if (!httpRes.ok && body.status !== "failure") {
153
+ body.status = "failure";
154
+ body.message = body.message ?? `HTTP ${httpRes.status}`;
155
+ }
156
+ if (body.status !== "success") {
157
+ return {
158
+ status: "failure",
159
+ message: body.message ?? "Failed to obtain access token"
160
+ };
161
+ }
162
+ const parsed = pickTokenPayload(body);
163
+ if (!parsed) {
164
+ return { status: "failure", message: "Token response missing access_token" };
165
+ }
166
+ return {
167
+ status: "success",
168
+ accessToken: parsed.accessToken,
169
+ expiresAtUtc: parsed.expiresAtUtc,
170
+ tokenType: parsed.tokenType
171
+ };
172
+ }
173
+ async function fetchAccessToken(apiKey, apiSecret) {
174
+ const store = getConfigStore();
175
+ const key = apiKey ?? store.apiKey;
176
+ const secret = apiSecret ?? store.apiSecret;
177
+ const result = await requestAccessToken(key, secret);
178
+ if (result.status === "success") {
179
+ store.setAccessToken(result.accessToken, result.expiresAtUtc);
180
+ }
181
+ return result;
182
+ }
183
+ async function ensureAccessToken() {
184
+ const store = getConfigStore();
185
+ const { accessToken, tokenExpiresAt, apiKey, apiSecret } = getAuthState();
186
+ if (accessToken && !isAccessTokenExpired(tokenExpiresAt)) {
187
+ return accessToken;
188
+ }
189
+ if (apiKey && apiSecret) {
190
+ const result = await fetchAccessToken(apiKey, apiSecret);
191
+ if (result.status === "success") return result.accessToken;
192
+ }
193
+ return accessToken;
194
+ }
195
+ function buildAuthHeaders(extra = {}) {
196
+ const headers = { ...extra };
197
+ const { consumer } = getConfigStore();
198
+ const { accessToken } = getAuthState();
199
+ if (!headers.Consumer && consumer) headers.Consumer = consumer;
200
+ if (!headers.Authorization && accessToken) {
201
+ headers.Authorization = `Bearer ${accessToken}`;
202
+ }
203
+ return headers;
204
+ }
205
+
48
206
  // src/config.js
49
207
  function configure(env) {
50
208
  const store = getConfigStore();
@@ -58,6 +216,33 @@ function getConfig() {
58
216
  function setBaseUrl(baseUrl) {
59
217
  getConfigStore().setBaseUrl(baseUrl);
60
218
  }
219
+ function setConsumer(consumer) {
220
+ getConfigStore().setConsumer(consumer);
221
+ }
222
+ function setAccessToken(accessToken, expiresAtUtc) {
223
+ getConfigStore().setAccessToken(accessToken, expiresAtUtc);
224
+ }
225
+ function clearAccessToken() {
226
+ getConfigStore().clearAccessToken();
227
+ }
228
+ function setApiCredentials(apiKey, apiSecret) {
229
+ getConfigStore().setApiCredentials(apiKey, apiSecret);
230
+ }
231
+ function clearApiCredentials() {
232
+ getConfigStore().clearApiCredentials();
233
+ }
234
+ function clearAuth() {
235
+ getConfigStore().clearAuth();
236
+ }
237
+ function getAuth() {
238
+ return getAuthState();
239
+ }
240
+ function fetchAccessToken2(apiKey, apiSecret) {
241
+ return fetchAccessToken(apiKey, apiSecret);
242
+ }
243
+ function ensureValidAccessToken() {
244
+ return ensureAccessToken();
245
+ }
61
246
 
62
247
  // src/apiRequest.js
63
248
  function buildQuery(params) {
@@ -74,10 +259,18 @@ function buildQuery(params) {
74
259
  return q ? `?${q}` : "";
75
260
  }
76
261
  async function request(baseUrl, fetchFn, path, options = {}) {
77
- const { method = "GET", headers = {}, body, query, skipContentType } = options;
262
+ const { method = "GET", headers = {}, body, query, skipContentType, skipAuth } = options;
78
263
  const url = `${String(baseUrl).replace(/\/+$/, "")}${path}${buildQuery(query)}`;
79
264
  const reqHeaders = { ...headers };
80
265
  if (!skipContentType && typeof body === "string") reqHeaders["Content-Type"] = "application/json";
266
+ if (!skipAuth) {
267
+ const store = getConfigStore();
268
+ if (!reqHeaders["Consumer"] && store.consumer) reqHeaders["Consumer"] = store.consumer;
269
+ const { accessToken } = getAuthState();
270
+ if (!reqHeaders["Authorization"] && accessToken) {
271
+ reqHeaders["Authorization"] = `Bearer ${accessToken}`;
272
+ }
273
+ }
81
274
  const res = await fetchFn(url, { method, headers: reqHeaders, body });
82
275
  const text = await res.text();
83
276
  let data;
@@ -90,23 +283,35 @@ async function request(baseUrl, fetchFn, path, options = {}) {
90
283
  data.status = "failure";
91
284
  data.message = data.message ?? `HTTP ${res.status}`;
92
285
  }
286
+ data._httpStatus = res.status;
93
287
  return data;
94
288
  }
95
- function mergeConsumerHeader(env, opts) {
289
+ function mergeRequestHeaders(env, opts) {
96
290
  const headers = { ...opts.headers || {} };
97
- if (!headers["Consumer"] && (env == null ? void 0 : env.consumer)) headers["Consumer"] = env.consumer;
291
+ const store = getConfigStore();
292
+ if (!headers["Consumer"] && ((env == null ? void 0 : env.consumer) || store.consumer)) {
293
+ headers["Consumer"] = (env == null ? void 0 : env.consumer) || store.consumer;
294
+ }
295
+ const { accessToken } = getAuthState();
296
+ if (!headers["Authorization"] && accessToken) {
297
+ headers["Authorization"] = `Bearer ${accessToken}`;
298
+ }
98
299
  return { ...opts, headers };
99
300
  }
301
+ async function executeRequest(baseUrl, fetchFn, path, opts) {
302
+ if (!opts.skipAuth) await ensureAccessToken();
303
+ return request(baseUrl, fetchFn, path, mergeRequestHeaders(null, opts));
304
+ }
100
305
  function createRequestHelpers(self, getEnv12) {
101
306
  const env = () => getEnv12(self);
102
307
  const baseUrl = () => env().baseUrl;
103
- const fetchFn = () => env().fetch ?? (typeof fetch !== "undefined" ? fetch : () => {
308
+ const fetchFn = () => env().fetch ?? getConfigStore().fetch ?? (typeof fetch !== "undefined" ? fetch : () => {
104
309
  throw new Error("fetch not available");
105
310
  });
106
311
  const req = (path, opts = {}) => {
107
312
  const url = baseUrl();
108
313
  if (!url) throw new Error("Model env requires baseUrl. Call configure({ baseUrl }) at app startup.");
109
- return request(url, fetchFn(), path, mergeConsumerHeader(env(), opts));
314
+ return executeRequest(url, fetchFn(), path, opts);
110
315
  };
111
316
  return {
112
317
  req,
@@ -117,14 +322,14 @@ function createRequestHelpers(self, getEnv12) {
117
322
  function createRequestHelpersFromEnv(env) {
118
323
  const e = env ?? getConfig();
119
324
  if (!e) throw new Error("Env required. Pass env to the method or call configure({ baseUrl }) at app startup.");
120
- const baseUrl = () => e == null ? void 0 : e.baseUrl;
121
- const fetchFn = () => (e == null ? void 0 : e.fetch) ?? (typeof fetch !== "undefined" ? fetch : () => {
325
+ const baseUrl = () => (e == null ? void 0 : e.baseUrl) ?? getConfigStore().baseUrl;
326
+ const fetchFn = () => (e == null ? void 0 : e.fetch) ?? getConfigStore().fetch ?? (typeof fetch !== "undefined" ? fetch : () => {
122
327
  throw new Error("fetch not available");
123
328
  });
124
329
  const req = (path, opts = {}) => {
125
330
  const url = baseUrl();
126
331
  if (!url) throw new Error("Env requires baseUrl. Call configure({ baseUrl }) at app startup.");
127
- return request(url, fetchFn(), path, mergeConsumerHeader(e, opts));
332
+ return executeRequest(url, fetchFn(), path, opts);
128
333
  };
129
334
  return {
130
335
  env: e,
@@ -1239,6 +1444,15 @@ var ParticipantModel = types11.model("Participant", {
1239
1444
  if (res.status === "success" && res.data) applySnapshot3(self, mapFromApi2(res.data));
1240
1445
  return res;
1241
1446
  },
1447
+ /** GET participant/getbyemail – fetch by companyKey + email on this snapshot */
1448
+ async getByEmail() {
1449
+ const res = await reqGet("/participant/getbyemail", {
1450
+ email: self.email,
1451
+ company_key: self.companyKey
1452
+ });
1453
+ if (res.status === "success" && res.data) applySnapshot3(self, mapFromApi2(res.data));
1454
+ return res;
1455
+ },
1242
1456
  /** POST participant/save – save participant (add or update) */
1243
1457
  async save() {
1244
1458
  const payload = toPayload(self);
@@ -1354,6 +1568,14 @@ function mapCalendarFromApi2(d) {
1354
1568
  modifiedOn: pick2("modifiedOn", "ModifiedOn", "modified_on") ?? null
1355
1569
  };
1356
1570
  }
1571
+ ParticipantModel.getByEmail = async (email, companyKey) => {
1572
+ const { reqGet } = createRequestHelpersFromEnv(getConfig());
1573
+ const res = await reqGet("/participant/getbyemail", { email, company_key: companyKey });
1574
+ if (res.status === "success" && res.data) {
1575
+ return ParticipantModel.create(mapFromApi2(res.data), { env: getConfig() });
1576
+ }
1577
+ return null;
1578
+ };
1357
1579
  ParticipantModel.get = async (participantId) => {
1358
1580
  const { reqGet } = createRequestHelpersFromEnv(getConfig());
1359
1581
  const res = await reqGet("/participant/get", { participant_id: participantId });
@@ -1532,6 +1754,7 @@ SettingModel.save = async (payload) => {
1532
1754
  SettingModel.uploadLogo = async (calendarId, file) => {
1533
1755
  const cfg = getConfig();
1534
1756
  if (!(cfg == null ? void 0 : cfg.baseUrl)) throw new Error("Configure baseUrl before uploadLogo");
1757
+ await ensureAccessToken();
1535
1758
  const fetchFn = cfg.fetch ?? (typeof fetch !== "undefined" ? fetch : () => {
1536
1759
  throw new Error("fetch not available");
1537
1760
  });
@@ -1541,6 +1764,7 @@ SettingModel.uploadLogo = async (calendarId, file) => {
1541
1764
  formData.append("file", file);
1542
1765
  const res = await fetchFn(url, {
1543
1766
  method: "POST",
1767
+ headers: buildAuthHeaders(),
1544
1768
  body: formData
1545
1769
  });
1546
1770
  const text = await res.text();
@@ -1663,6 +1887,7 @@ AssetModel.upload = async (file, opts = {}) => {
1663
1887
  if (!file) return { status: "failure", message: "file is required" };
1664
1888
  const category = opts.category != null ? String(opts.category).trim() : "";
1665
1889
  if (!category) return { status: "failure", message: "category is required" };
1890
+ await ensureAccessToken();
1666
1891
  const fetchFn = cfg.fetch ?? (typeof fetch !== "undefined" ? fetch : () => {
1667
1892
  throw new Error("fetch not available");
1668
1893
  });
@@ -1677,8 +1902,7 @@ AssetModel.upload = async (file, opts = {}) => {
1677
1902
  if (opts.calendarId != null && String(opts.calendarId).trim() !== "") {
1678
1903
  formData.append("calendar_id", String(opts.calendarId).trim());
1679
1904
  }
1680
- const headers = {};
1681
- if (cfg.consumer) headers.Consumer = cfg.consumer;
1905
+ const headers = buildAuthHeaders();
1682
1906
  if (opts.consumer != null && String(opts.consumer).trim() !== "") {
1683
1907
  headers.Consumer = String(opts.consumer).trim();
1684
1908
  }
@@ -2734,12 +2958,25 @@ export {
2734
2958
  RecurringFrequency,
2735
2959
  RootStore,
2736
2960
  Setting_default as SettingModel,
2961
+ TOKEN_PATH,
2737
2962
  TimeFrame_default as TimeFrameModel,
2738
2963
  TimeSlot_default as TimeSlotModel,
2739
2964
  Unit,
2965
+ buildAuthHeaders,
2966
+ clearAccessToken,
2967
+ clearApiCredentials,
2968
+ clearAuth,
2740
2969
  configure,
2741
2970
  createRootStore,
2971
+ ensureValidAccessToken,
2972
+ fetchAccessToken2 as fetchAccessToken,
2973
+ getAuth,
2742
2974
  getConfig,
2743
2975
  getConfigStore,
2744
- setBaseUrl
2976
+ isAccessTokenExpired,
2977
+ requestAccessToken,
2978
+ setAccessToken,
2979
+ setApiCredentials,
2980
+ setBaseUrl,
2981
+ setConsumer
2745
2982
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blazeo.com/calendar-client",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Blazeo Calendar / Appointment API client with MobX State Tree models",
5
5
  "exports": {
6
6
  ".": {