feeef 0.8.4 → 0.8.9

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
@@ -2,6 +2,14 @@
2
2
 
3
3
  `feeefjs` is a TypeScript library for managing feeef e-commerce platforms for self-hosted stores. It provides a wrapper for feeef rest api such like send order..., also have frontend srvices like the `CartService` class for managing cart items, shipping methods, and calculating totals. The library also includes a `NotifiableService` base class for handling listeners that react to changes in the service state.
4
4
 
5
+ ## Developer OAuth (third-party apps)
6
+
7
+ Use `OAuthRepository.buildAuthorizeUrl` (point `baseUrl` at the **accounts** host, e.g. `https://accounts.feeef.org`) and `FeeeF` client `oauth.exchangeAuthorizationCode` / `revokeToken` / `introspectToken` against the **API** base (`.../v1`). Full flow, scopes, and troubleshooting: Feeef backend **`docs/OAUTH2_DEVELOPER.md`** (in the Adonis API repo).
8
+
9
+ ## Realtime (AdonisJS Transmit)
10
+
11
+ Server-Sent Events use the same **API origin** as the REST client, but **without** the `/v1` or `/api/v1` prefix (e.g. `https://api.feeef.org/__transmit/events`). Use `createFeeefTransmit({ apiBaseUrl, getAccessToken })` so subscribe/unsubscribe POSTs get a proper `Authorization: Bearer …` header. The underlying client is the official [`@adonisjs/transmit-client`](https://www.npmjs.com/package/@adonisjs/transmit-client) (`Transmit`, `Subscription` are re-exported).
12
+
5
13
  ---
6
14
 
7
15
  # CartService & NotifiableService
@@ -213,3 +221,76 @@ interface ProductOffer {
213
221
  ## Contributing
214
222
 
215
223
  Feel free to fork this repository and contribute improvements, bug fixes, or additional features. Open a pull request for any major changes!
224
+
225
+ ## Developer OAuth (Authorization Code)
226
+
227
+ Use this flow when a third-party app (created in `apps`) needs user authorization.
228
+
229
+ ### Endpoints
230
+
231
+ - Authorize: `GET /v1/oauth/authorize`
232
+ - Token: `POST /v1/oauth/token`
233
+ - Revoke: `POST /v1/oauth/revoke`
234
+ - Introspect: `POST /v1/oauth/introspect`
235
+
236
+ ### Step-by-step (Google-like)
237
+
238
+ 1. Build authorize URL:
239
+
240
+ ```ts
241
+ import { FeeeF, OAuthRepository } from 'feeef'
242
+
243
+ const feeef = new FeeeF({
244
+ apiKey: '<your_api_key>',
245
+ baseURL: 'https://api.feeef.org/v1',
246
+ })
247
+
248
+ const authorizeUrl = OAuthRepository.buildAuthorizeUrl({
249
+ baseUrl: 'https://api.feeef.org/v1',
250
+ clientId: '<client_id>',
251
+ redirectUri: 'https://your-app.com/oauth/callback',
252
+ scope: ['*'], // or explicit scopes
253
+ state: crypto.randomUUID(),
254
+ })
255
+ ```
256
+
257
+ 2. Open `authorizeUrl` in browser.
258
+ 3. If user is not signed in, API may return `401` with:
259
+ - `error: "login_required"`
260
+ - `login_url` (accounts sign-in URL with `next`)
261
+ 4. Redirect user to `login_url`.
262
+ 5. After sign-in + authorization, user is redirected to your callback with `code` (and `state`).
263
+ 6. Exchange `code` for token:
264
+
265
+ ```ts
266
+ const tokenResponse = await feeef.oauth.exchangeAuthorizationCode({
267
+ code: '<code_from_callback>',
268
+ redirectUri: 'https://your-app.com/oauth/callback',
269
+ clientId: '<client_id>',
270
+ clientSecret: '<client_secret>',
271
+ })
272
+
273
+ await feeef.oauth.revokeToken(tokenResponse.access_token)
274
+
275
+ const introspection = await feeef.oauth.introspectToken(tokenResponse.access_token)
276
+ console.log(introspection.active)
277
+ ```
278
+
279
+ Alternative static helper:
280
+
281
+ ```ts
282
+ import { OAuthRepository } from 'feeef'
283
+
284
+ const authorizeUrl = OAuthRepository.buildAuthorizeUrl({
285
+ baseUrl: 'https://api.feeef.org/v1',
286
+ clientId: '<client_id>',
287
+ redirectUri: 'https://your-app.com/oauth/callback',
288
+ })
289
+ ```
290
+
291
+ ### Important
292
+
293
+ - `redirect_uri` must exactly match one of the app's registered redirect URIs.
294
+ - Keep `client_secret` server-side only.
295
+ - Always validate `state` on callback.
296
+ - Prefer PKCE (`code_challenge`, `code_verifier`) for public clients.
package/build/index.js CHANGED
@@ -141,7 +141,12 @@ var OrderRepository = class extends ModelRepository {
141
141
  if (options.page) params.page = options.page;
142
142
  if (options.offset) params.offset = options.offset;
143
143
  if (options.limit) params.limit = options.limit;
144
- if (options.storeId) params.store_id = options.storeId;
144
+ const useMultiStore = options.storeIds != null && options.storeIds.length > 0;
145
+ if (useMultiStore) {
146
+ params.store_ids = options.storeIds;
147
+ } else if (options.storeId) {
148
+ params.store_id = options.storeId;
149
+ }
145
150
  if (options.status) {
146
151
  params.status = Array.isArray(options.status) ? options.status : [options.status];
147
152
  }
@@ -841,6 +846,17 @@ var AppRepository = class extends ModelRepository {
841
846
  }
842
847
  /**
843
848
  * Builds the OAuth authorize URL to which the user should be redirected.
849
+ * This is the first step of the authorization-code flow (similar UX to Google OAuth).
850
+ *
851
+ * Production: opening this URL on the **API** host (`api.*`) issues a redirect to the same path on
852
+ * **accounts.*** so the consent screen appears on the trusted accounts domain; query params are preserved.
853
+ *
854
+ * If the user is not logged in yet, API `GET /oauth/authorize` returns:
855
+ * - `401 login_required`
856
+ * - `login_url` (accounts sign-in URL with `next=...`)
857
+ *
858
+ * The client should navigate to `login_url`, let the user sign in, and then
859
+ * continue by opening the original authorize URL again (or rely on `next`).
844
860
  *
845
861
  * @param params - Parameters for the authorize URL.
846
862
  * @param params.baseUrl - API base URL (e.g. https://api.feeef.org/api/v1).
@@ -867,6 +883,120 @@ var AppRepository = class extends ModelRepository {
867
883
  }
868
884
  return url.toString();
869
885
  }
886
+ /**
887
+ * Public `apps.public_data` (no auth). Pass [config] to skip Authorization if your axios instance adds it globally.
888
+ */
889
+ async getPublicData(id, config) {
890
+ const res = await this.client.get(`/${this.resource}/${id}/public-data`, config);
891
+ return res.data;
892
+ }
893
+ async getPrivateData(id) {
894
+ const res = await this.client.get(`/${this.resource}/${id}/private-data`);
895
+ return res.data;
896
+ }
897
+ async putPublicData(id, publicNamespaces) {
898
+ const res = await this.client.put(`/${this.resource}/${id}/public-data`, {
899
+ public: publicNamespaces
900
+ });
901
+ return res.data;
902
+ }
903
+ async putPrivateData(id, privateNamespaces) {
904
+ const res = await this.client.put(`/${this.resource}/${id}/private-data`, {
905
+ private: privateNamespaces
906
+ });
907
+ return res.data;
908
+ }
909
+ async getUserDataMe(id) {
910
+ const res = await this.client.get(`/${this.resource}/${id}/user-data/me`);
911
+ return res.data;
912
+ }
913
+ async putUserDataMe(id, publicNamespaces) {
914
+ const res = await this.client.put(`/${this.resource}/${id}/user-data/me`, {
915
+ public: publicNamespaces
916
+ });
917
+ return res.data;
918
+ }
919
+ async getUserDataForUser(appId, userId) {
920
+ const res = await this.client.get(`/${this.resource}/${appId}/user-data/users/${userId}`);
921
+ return res.data;
922
+ }
923
+ async putUserDataForUser(appId, userId, body) {
924
+ const res = await this.client.put(`/${this.resource}/${appId}/user-data/users/${userId}`, body);
925
+ return res.data;
926
+ }
927
+ };
928
+
929
+ // src/feeef/repositories/oauth.ts
930
+ var OAuthRepository = class {
931
+ client;
932
+ constructor(client) {
933
+ this.client = client;
934
+ }
935
+ /**
936
+ * Builds the authorize URL for browser redirect.
937
+ */
938
+ static buildAuthorizeUrl(params) {
939
+ const base = params.baseUrl.endsWith("/") ? params.baseUrl : `${params.baseUrl}/`;
940
+ const url = new URL("oauth/authorize", base);
941
+ url.searchParams.set("client_id", params.clientId);
942
+ url.searchParams.set("redirect_uri", params.redirectUri);
943
+ url.searchParams.set("response_type", "code");
944
+ if (params.scope?.length) url.searchParams.set("scope", params.scope.join(" "));
945
+ if (params.state) url.searchParams.set("state", params.state);
946
+ if (params.codeChallenge) url.searchParams.set("code_challenge", params.codeChallenge);
947
+ if (params.codeChallengeMethod) {
948
+ url.searchParams.set("code_challenge_method", params.codeChallengeMethod);
949
+ }
950
+ return url.toString();
951
+ }
952
+ /**
953
+ * Exchanges an authorization code for an access token.
954
+ */
955
+ async exchangeAuthorizationCode(params) {
956
+ const body = new URLSearchParams({
957
+ grant_type: "authorization_code",
958
+ code: params.code,
959
+ redirect_uri: params.redirectUri,
960
+ client_id: params.clientId,
961
+ client_secret: params.clientSecret
962
+ });
963
+ if (params.codeVerifier) {
964
+ body.set("code_verifier", params.codeVerifier);
965
+ }
966
+ const response = await this.client.post("/oauth/token", body.toString(), {
967
+ headers: {
968
+ "Content-Type": "application/x-www-form-urlencoded"
969
+ }
970
+ });
971
+ return response.data;
972
+ }
973
+ /**
974
+ * Revokes an OAuth token.
975
+ */
976
+ async revokeToken(token, tokenTypeHint) {
977
+ const body = new URLSearchParams({ token });
978
+ if (tokenTypeHint) {
979
+ body.set("token_type_hint", tokenTypeHint);
980
+ }
981
+ const response = await this.client.post("/oauth/revoke", body.toString(), {
982
+ headers: {
983
+ "Content-Type": "application/x-www-form-urlencoded"
984
+ }
985
+ });
986
+ return response.data;
987
+ }
988
+ /**
989
+ * Introspects an OAuth token.
990
+ */
991
+ async introspectToken(token) {
992
+ const body = new URLSearchParams({ token });
993
+ const response = await this.client.post("/oauth/introspect", body.toString(), {
994
+ headers: {
995
+ "Content-Type": "application/x-www-form-urlencoded"
996
+ }
997
+ });
998
+ return response.data;
999
+ }
870
1000
  };
871
1001
 
872
1002
  // src/feeef/repositories/deposits.ts
@@ -1017,9 +1147,17 @@ var PromoRepository = class {
1017
1147
  if (params?.page != null) query.page = params.page;
1018
1148
  if (params?.limit != null) query.limit = params.limit;
1019
1149
  if (params?.validNow === true) query.validNow = "1";
1150
+ if (params?.filterator) query.filterator = params.filterator;
1020
1151
  const res = await this.client.get("/promos", { params: query });
1021
1152
  return res.data;
1022
1153
  }
1154
+ /**
1155
+ * Fetches a single promo by id.
1156
+ */
1157
+ async find(params) {
1158
+ const res = await this.client.get(`/promos/${params.id}`);
1159
+ return res.data;
1160
+ }
1023
1161
  /**
1024
1162
  * Validates a promo code. Returns validation result with discount info or reason.
1025
1163
  */
@@ -3513,6 +3651,10 @@ var FeeeF = class {
3513
3651
  * The repository for managing developer-registered apps (OAuth clients).
3514
3652
  */
3515
3653
  apps;
3654
+ /**
3655
+ * The repository for OAuth2 authorize/token/revoke/introspect operations.
3656
+ */
3657
+ oauth;
3516
3658
  /**
3517
3659
  * The repository for managing orders.
3518
3660
  */
@@ -3604,6 +3746,7 @@ var FeeeF = class {
3604
3746
  this.imageGenerations = new ImageGenerationsRepository(this.client);
3605
3747
  this.users = new UserRepository(this.client);
3606
3748
  this.apps = new AppRepository(this.client);
3749
+ this.oauth = new OAuthRepository(this.client);
3607
3750
  this.orders = new OrderRepository(this.client);
3608
3751
  this.deposits = new DepositRepository(this.client);
3609
3752
  this.transfers = new TransferRepository(this.client);
@@ -3957,6 +4100,77 @@ var EmbaddedContactType = /* @__PURE__ */ ((EmbaddedContactType2) => {
3957
4100
  return EmbaddedContactType2;
3958
4101
  })(EmbaddedContactType || {});
3959
4102
 
4103
+ // src/realtime/transmit.ts
4104
+ import { Transmit } from "@adonisjs/transmit-client";
4105
+ import { Subscription, Transmit as Transmit2 } from "@adonisjs/transmit-client";
4106
+ function transmitRootFromApiBaseUrl(apiBaseUrl) {
4107
+ let u = apiBaseUrl.trim().replace(/\/+$/, "");
4108
+ if (u.endsWith("/api/v1")) {
4109
+ u = u.slice(0, -"/api/v1".length);
4110
+ } else if (u.endsWith("/v1")) {
4111
+ u = u.slice(0, -"/v1".length);
4112
+ }
4113
+ return u;
4114
+ }
4115
+ function retrieveXsrfTokenFromCookie() {
4116
+ const cookie = globalThis.document?.cookie;
4117
+ if (!cookie) return null;
4118
+ const match = cookie.match(new RegExp("(^|;\\s*)(XSRF-TOKEN)=([^;]*)"));
4119
+ return match ? decodeURIComponent(match[3]) : null;
4120
+ }
4121
+ var FeeefTransmitHttpClient = class {
4122
+ constructor(options, getAuthorizationHeader) {
4123
+ this.options = options;
4124
+ this.getAuthorizationHeader = getAuthorizationHeader;
4125
+ }
4126
+ send(request) {
4127
+ return fetch(request);
4128
+ }
4129
+ createRequest(path, body) {
4130
+ const headers = {
4131
+ "Content-Type": "application/json",
4132
+ "X-XSRF-TOKEN": retrieveXsrfTokenFromCookie() ?? ""
4133
+ };
4134
+ const auth = this.getAuthorizationHeader();
4135
+ if (auth) {
4136
+ headers.Authorization = auth.startsWith("Bearer ") ? auth : `Bearer ${auth}`;
4137
+ }
4138
+ return new Request(`${this.options.baseUrl}${path}`, {
4139
+ method: "POST",
4140
+ headers,
4141
+ body: JSON.stringify({ uid: this.options.uid, ...body }),
4142
+ credentials: "include"
4143
+ });
4144
+ }
4145
+ };
4146
+ function createFeeefTransmit(options) {
4147
+ const baseUrl = transmitRootFromApiBaseUrl(options.apiBaseUrl);
4148
+ const getToken = options.getAccessToken ?? (() => void 0);
4149
+ return new Transmit({
4150
+ ...options.transmit,
4151
+ baseUrl,
4152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Transmit’s HttpClient is not exported; FeeefTransmitHttpClient matches its shape.
4153
+ httpClientFactory: (url, uid) => new FeeefTransmitHttpClient({ baseUrl: url, uid }, () => {
4154
+ const t = getToken();
4155
+ if (t === null || t === void 0 || t === "") return void 0;
4156
+ const s = String(t).trim();
4157
+ return s.startsWith("Bearer ") ? s : `Bearer ${s}`;
4158
+ })
4159
+ });
4160
+ }
4161
+ function createFeeefTransmitFromAxios(client, options) {
4162
+ const apiBaseUrl = client.defaults.baseURL ?? "";
4163
+ return createFeeefTransmit({
4164
+ ...options,
4165
+ apiBaseUrl,
4166
+ getAccessToken: () => {
4167
+ const raw = client.defaults.headers.common?.Authorization;
4168
+ if (typeof raw !== "string") return void 0;
4169
+ return raw.replace(/^Bearer\s+/i, "").trim() || void 0;
4170
+ }
4171
+ });
4172
+ }
4173
+
3960
4174
  // src/utils.ts
3961
4175
  var convertDartColorToCssNumber = (dartColor) => {
3962
4176
  const alpha = dartColor >> 24 & 255;
@@ -4059,6 +4273,7 @@ export {
4059
4273
  FeedbackRepository,
4060
4274
  FeedbackStatus,
4061
4275
  FeeeF,
4276
+ FeeefTransmitHttpClient,
4062
4277
  GoogleSheetIntegrationApi,
4063
4278
  ImageGenerationsRepository,
4064
4279
  ImagePromptTemplatesRepository,
@@ -4067,6 +4282,7 @@ export {
4067
4282
  ModelRepository,
4068
4283
  NoestDeliveryIntegrationApi,
4069
4284
  NotificationsService,
4285
+ OAuthRepository,
4070
4286
  OrderRepository,
4071
4287
  OrderStatus,
4072
4288
  PaymentStatus,
@@ -4092,8 +4308,10 @@ export {
4092
4308
  StoreRepository,
4093
4309
  StoreSubscriptionStatus,
4094
4310
  StoreSubscriptionType,
4311
+ Subscription,
4095
4312
  TiktokPixelEvent,
4096
4313
  TransferRepository,
4314
+ Transmit2 as Transmit,
4097
4315
  UserRepository,
4098
4316
  VariantOptionType,
4099
4317
  WebhookEvent,
@@ -4102,6 +4320,8 @@ export {
4102
4320
  ZimouDeliveryIntegrationApi,
4103
4321
  convertDartColorToCssNumber,
4104
4322
  convertOrderEntityToOrderTrackEntity,
4323
+ createFeeefTransmit,
4324
+ createFeeefTransmitFromAxios,
4105
4325
  createImageGenerationFormData,
4106
4326
  cssColorToHslString,
4107
4327
  dartColorToCssColor,
@@ -4137,6 +4357,7 @@ export {
4137
4357
  serializeAttachmentPayloads,
4138
4358
  serializeImagePromptTemplateCreate,
4139
4359
  serializeImagePromptTemplateUpdate,
4360
+ transmitRootFromApiBaseUrl,
4140
4361
  tryFixPhoneNumber,
4141
4362
  validatePhoneNumber
4142
4363
  };