@tandem-language-exchange/content-store 1.3.2 → 1.3.3

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/dist/node.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { S as SDKConfig, F as FetchCmsBundlesOptions, a as FetchTranslationBundlesOptions, T as TranslationBundleInfo, b as FetchMergedTranslationBundlesOptions, C as CMSProvider, Q as QueryOptions } from './index-DqDNlXSE.js';
1
+ import { C as CMSProvider, T as TranslationBundleInfo, F as FetchCmsBundlesOptions, a as FetchTranslationBundlesOptions, S as SDKConfig, b as FetchMergedTranslationBundlesOptions, Q as QueryOptions } from './index-DqDNlXSE.js';
2
2
  export { B as BundleItem, c as CmsBundleInfo, d as ContentStore, e as S3Config, f as S3RetryConfig, g as TranslationFilterConfig, h as fetchCmsBundles, i as fetchMergedTranslationBundles, j as fetchTranslationBundles, k as getDefaultS3RetryConfig, q as queryCmsBundle } from './index-DqDNlXSE.js';
3
3
 
4
4
  /**
@@ -11,6 +11,65 @@ export { B as BundleItem, c as CmsBundleInfo, d as ContentStore, e as S3Config,
11
11
  */
12
12
  declare function trimDepth(value: unknown, remaining: number): unknown;
13
13
 
14
+ type ContentRefreshScope = 'cms' | 'translations' | 'all';
15
+ interface ContentRefreshRequest {
16
+ scope?: ContentRefreshScope;
17
+ cms?: CMSProvider;
18
+ content_types?: string[];
19
+ projects?: string[];
20
+ locales?: string[];
21
+ }
22
+ /** Defaults applied when the request omits fields (set by the host application). */
23
+ interface ContentRefreshDefaults {
24
+ scope?: ContentRefreshScope;
25
+ cms?: CMSProvider;
26
+ contentTypes?: string[];
27
+ translationProjects?: string[];
28
+ locales?: string[];
29
+ }
30
+ interface ContentRefreshError {
31
+ step: string;
32
+ message: string;
33
+ }
34
+ interface ContentRefreshResult {
35
+ ok: boolean;
36
+ scope: ContentRefreshScope;
37
+ cmsFiles?: Record<string, string>;
38
+ translationFiles?: TranslationBundleInfo;
39
+ errors: ContentRefreshError[];
40
+ durationMs: number;
41
+ }
42
+ interface ContentRefreshFetchers {
43
+ fetchCmsBundles: (options: FetchCmsBundlesOptions) => Promise<Record<string, string>>;
44
+ fetchTranslationBundles: (options: FetchTranslationBundlesOptions) => Promise<TranslationBundleInfo>;
45
+ }
46
+ /** Base64 of `username:password` (value only — prefix with `Basic ` in the header). */
47
+ declare function encodeBasicAuthCredentials(username: string, password: string): string;
48
+ /**
49
+ * Validates `Authorization: Basic <base64>` against the expected credentials
50
+ * (base64 of `user:pass`, same as staging site basic auth).
51
+ */
52
+ declare function assertContentRefreshBasicAuth(authorizationHeader: string | undefined, expectedCredentialsBase64: string): void;
53
+ declare class ContentRefreshAuthError extends Error {
54
+ readonly statusCode: number;
55
+ constructor(message: string, statusCode: number);
56
+ }
57
+ /**
58
+ * Pull the latest bundles from S3 into the host app's configured output directory.
59
+ */
60
+ declare function executeContentRefresh(fetchers: ContentRefreshFetchers, request: ContentRefreshRequest, defaults?: ContentRefreshDefaults): Promise<ContentRefreshResult>;
61
+ interface PostContentRefreshResponse {
62
+ target: string;
63
+ ok: boolean;
64
+ status: number;
65
+ body?: unknown;
66
+ error?: string;
67
+ }
68
+ /**
69
+ * Ask a remote application (web-site, web-app, …) to refresh its local content cache.
70
+ */
71
+ declare function postContentRefresh(target: string, url: string, basicAuth: string, request: ContentRefreshRequest): Promise<PostContentRefreshResponse>;
72
+
14
73
  declare class ContentStoreSDK {
15
74
  private store;
16
75
  private outputDir;
@@ -39,6 +98,41 @@ declare class ContentStoreSDK {
39
98
  * Queries a previously fetched bundle from the local filesystem.
40
99
  */
41
100
  queryCmsBundle(cms: CMSProvider, contentType: string, options?: QueryOptions): Promise<unknown[]>;
101
+ /**
102
+ * Download the latest bundles from S3 (same as the hosted refresh HTTP endpoint).
103
+ */
104
+ refreshContent(request?: ContentRefreshRequest, defaults?: ContentRefreshDefaults): Promise<ContentRefreshResult>;
105
+ }
106
+
107
+ /** Minimal request shape for Next.js Pages API routes (`pages/api/*`). */
108
+ interface NextPagesContentRefreshRequest {
109
+ method?: string;
110
+ headers: {
111
+ authorization?: string | string[];
112
+ };
113
+ body?: ContentRefreshRequest;
42
114
  }
115
+ /** Minimal response shape for Next.js Pages API routes. */
116
+ interface NextPagesContentRefreshResponse {
117
+ status(code: number): {
118
+ json(body: unknown): void;
119
+ };
120
+ }
121
+ interface NextContentRefreshHandlerConfig {
122
+ sdk: ContentStoreSDK;
123
+ /** Base64 of `username:password` (same value as `CONTENT_REFRESH_BASIC_AUTH`). */
124
+ basicAuth: string;
125
+ defaults?: ContentRefreshDefaults;
126
+ }
127
+ /** @deprecated Use {@link NextContentRefreshHandlerConfig}. */
128
+ type NextPagesContentRefreshHandlerConfig = NextContentRefreshHandlerConfig;
129
+ /**
130
+ * Handler for a Next.js Pages API route (e.g. pages/api/internal/content-refresh.ts).
131
+ */
132
+ declare function createNextPagesApiContentRefreshHandler(config: NextContentRefreshHandlerConfig): (req: NextPagesContentRefreshRequest, res: NextPagesContentRefreshResponse) => Promise<void>;
133
+ /**
134
+ * Handler for a Next.js App Router route (e.g. app/api/internal/content-refresh/route.ts).
135
+ */
136
+ declare function handleNextAppRouterContentRefresh(request: Request, config: NextContentRefreshHandlerConfig): Promise<Response>;
43
137
 
44
- export { CMSProvider, ContentStoreSDK, FetchCmsBundlesOptions, FetchMergedTranslationBundlesOptions, FetchTranslationBundlesOptions, QueryOptions, SDKConfig, TranslationBundleInfo, trimDepth };
138
+ export { CMSProvider, ContentRefreshAuthError, type ContentRefreshDefaults, type ContentRefreshRequest, type ContentRefreshResult, type ContentRefreshScope, ContentStoreSDK, FetchCmsBundlesOptions, FetchMergedTranslationBundlesOptions, FetchTranslationBundlesOptions, type NextContentRefreshHandlerConfig, type NextPagesContentRefreshHandlerConfig, type NextPagesContentRefreshRequest, type NextPagesContentRefreshResponse, type PostContentRefreshResponse, QueryOptions, SDKConfig, TranslationBundleInfo, assertContentRefreshBasicAuth, createNextPagesApiContentRefreshHandler, encodeBasicAuthCredentials, executeContentRefresh, handleNextAppRouterContentRefresh, postContentRefresh, trimDepth };
package/dist/node.js CHANGED
@@ -13,9 +13,7 @@ var ContentStore = class {
13
13
  credentials: {
14
14
  accessKeyId: cfg.accessKeyId,
15
15
  secretAccessKey: cfg.secretAccessKey
16
- },
17
- requestChecksumCalculation: "WHEN_REQUIRED",
18
- responseChecksumValidation: "WHEN_REQUIRED"
16
+ }
19
17
  });
20
18
  this.bucket = cfg.bucket;
21
19
  }
@@ -179,7 +177,7 @@ function trimDepth(value, remaining) {
179
177
  }
180
178
 
181
179
  // src/shared/lingohub.ts
182
- var defaultLocales = ["en", "fr", "de", "es", "it", "pt-br", "ru", "zh-hans", "zh-hant", "ko", "ja"];
180
+ var defaultLocales = ["en", "fr", "de", "es", "it", "pt-br", "uk", "ru", "zh-hans", "zh-hant", "ko", "ja"];
183
181
  var localeMapping = {
184
182
  ios: {
185
183
  "pt-br": "pt",
@@ -232,7 +230,7 @@ var allProjects = {
232
230
  },
233
231
  {
234
232
  resource: "ipad",
235
- fileName: "MainiPad.[locale].strings",
233
+ fileName: "Main_iPad.[locale].strings",
236
234
  type: "strings",
237
235
  localeMapping: localeMapping.ios
238
236
  },
@@ -744,6 +742,137 @@ async function queryCmsBundle(outputDir, cms, contentType, options = {}) {
744
742
  return items;
745
743
  }
746
744
 
745
+ // src/shared/content-refresh.ts
746
+ import { timingSafeEqual } from "crypto";
747
+ function resolveContentRefreshScope(request, defaults) {
748
+ return request.scope ?? defaults.scope ?? "cms";
749
+ }
750
+ function encodeBasicAuthCredentials(username, password) {
751
+ return Buffer.from(`${username}:${password}`, "utf8").toString("base64");
752
+ }
753
+ function assertContentRefreshBasicAuth(authorizationHeader, expectedCredentialsBase64) {
754
+ if (!expectedCredentialsBase64) {
755
+ throw new ContentRefreshAuthError(
756
+ "Content refresh basic auth is not configured (set CONTENT_REFRESH_BASIC_AUTH)",
757
+ 500
758
+ );
759
+ }
760
+ if (!authorizationHeader?.startsWith("Basic ")) {
761
+ throw new ContentRefreshAuthError(
762
+ "Missing or malformed Authorization header (expected Basic)",
763
+ 401
764
+ );
765
+ }
766
+ const provided = authorizationHeader.slice(6).trim();
767
+ const bufA = new TextEncoder().encode(provided);
768
+ const bufB = new TextEncoder().encode(expectedCredentialsBase64);
769
+ if (bufA.byteLength !== bufB.byteLength || !timingSafeEqual(bufA, bufB)) {
770
+ throw new ContentRefreshAuthError("Invalid basic auth credentials", 403);
771
+ }
772
+ }
773
+ var ContentRefreshAuthError = class extends Error {
774
+ constructor(message, statusCode) {
775
+ super(message);
776
+ this.statusCode = statusCode;
777
+ this.name = "ContentRefreshAuthError";
778
+ }
779
+ };
780
+ async function executeContentRefresh(fetchers, request, defaults = {}) {
781
+ const started = Date.now();
782
+ const scope = resolveContentRefreshScope(request, defaults);
783
+ const errors = [];
784
+ let cmsFiles;
785
+ let translationFiles;
786
+ if (scope === "cms" || scope === "all") {
787
+ const cms = request.cms ?? defaults.cms ?? "contentful";
788
+ const contentTypes = request.content_types ?? defaults.contentTypes;
789
+ if (!contentTypes?.length) {
790
+ errors.push({
791
+ step: "cms",
792
+ message: "content_types (or handler defaults.contentTypes) is required for CMS refresh"
793
+ });
794
+ } else {
795
+ try {
796
+ cmsFiles = await fetchers.fetchCmsBundles({ cms, contentTypes });
797
+ } catch (err) {
798
+ errors.push({
799
+ step: "cms",
800
+ message: err instanceof Error ? err.message : String(err)
801
+ });
802
+ }
803
+ }
804
+ }
805
+ if (scope === "translations" || scope === "all") {
806
+ const projects = request.projects ?? defaults.translationProjects;
807
+ if (!projects?.length) {
808
+ errors.push({
809
+ step: "translations",
810
+ message: "projects (or handler defaults.translationProjects) is required for translation refresh"
811
+ });
812
+ } else {
813
+ const locales = request.locales ?? defaults.locales;
814
+ try {
815
+ const projectMap = Object.fromEntries(
816
+ projects.map((p) => [p, []])
817
+ );
818
+ translationFiles = await fetchers.fetchTranslationBundles({
819
+ projects: projectMap,
820
+ locales
821
+ });
822
+ } catch (err) {
823
+ errors.push({
824
+ step: "translations",
825
+ message: err instanceof Error ? err.message : String(err)
826
+ });
827
+ }
828
+ }
829
+ }
830
+ return {
831
+ ok: errors.length === 0,
832
+ scope,
833
+ cmsFiles,
834
+ translationFiles,
835
+ errors,
836
+ durationMs: Date.now() - started
837
+ };
838
+ }
839
+ async function postContentRefresh(target, url, basicAuth, request) {
840
+ try {
841
+ const response = await fetch(url, {
842
+ method: "POST",
843
+ headers: {
844
+ Authorization: `Basic ${basicAuth}`,
845
+ "Content-Type": "application/json",
846
+ Accept: "application/json"
847
+ },
848
+ body: JSON.stringify(request)
849
+ });
850
+ const text = await response.text();
851
+ let body;
852
+ if (text) {
853
+ try {
854
+ body = JSON.parse(text);
855
+ } catch {
856
+ body = text;
857
+ }
858
+ }
859
+ return {
860
+ target,
861
+ ok: response.ok,
862
+ status: response.status,
863
+ body,
864
+ error: response.ok ? void 0 : typeof body === "object" && body !== null && "error" in body ? String(body.error) : `HTTP ${response.status}`
865
+ };
866
+ } catch (err) {
867
+ return {
868
+ target,
869
+ ok: false,
870
+ status: 0,
871
+ error: err instanceof Error ? err.message : String(err)
872
+ };
873
+ }
874
+ }
875
+
747
876
  // src/sdk/client.ts
748
877
  var ContentStoreSDK = class {
749
878
  store;
@@ -784,14 +913,97 @@ var ContentStoreSDK = class {
784
913
  async queryCmsBundle(cms, contentType, options = {}) {
785
914
  return queryCmsBundle(this.outputDir, cms, contentType, options);
786
915
  }
916
+ /**
917
+ * Download the latest bundles from S3 (same as the hosted refresh HTTP endpoint).
918
+ */
919
+ async refreshContent(request = {}, defaults = {}) {
920
+ return executeContentRefresh(
921
+ {
922
+ fetchCmsBundles: (opts) => this.fetchCmsBundles(opts),
923
+ fetchTranslationBundles: (opts) => this.fetchTranslationBundles(opts)
924
+ },
925
+ request,
926
+ defaults
927
+ );
928
+ }
787
929
  };
930
+
931
+ // src/sdk/next-content-refresh.ts
932
+ function readAuthorizationHeader(value) {
933
+ if (value == null) {
934
+ return void 0;
935
+ }
936
+ if (Array.isArray(value)) {
937
+ return value[0];
938
+ }
939
+ return value;
940
+ }
941
+ async function runContentRefresh(config, authorization, body) {
942
+ const { sdk, basicAuth, defaults = {} } = config;
943
+ try {
944
+ assertContentRefreshBasicAuth(authorization, basicAuth);
945
+ } catch (err) {
946
+ if (err instanceof ContentRefreshAuthError) {
947
+ return { status: err.statusCode, body: { error: err.message } };
948
+ }
949
+ throw err;
950
+ }
951
+ const result = await executeContentRefresh(
952
+ {
953
+ fetchCmsBundles: (opts) => sdk.fetchCmsBundles(opts),
954
+ fetchTranslationBundles: (opts) => sdk.fetchTranslationBundles(opts)
955
+ },
956
+ body,
957
+ defaults
958
+ );
959
+ const status = result.ok ? 200 : result.errors.length > 0 ? 207 : 500;
960
+ return { status, body: result };
961
+ }
962
+ function createNextPagesApiContentRefreshHandler(config) {
963
+ return async (req, res) => {
964
+ if (req.method !== "POST") {
965
+ res.status(405).json({ error: "Method not allowed" });
966
+ return;
967
+ }
968
+ const { status, body } = await runContentRefresh(
969
+ config,
970
+ readAuthorizationHeader(req.headers.authorization),
971
+ req.body ?? {}
972
+ );
973
+ res.status(status).json(body);
974
+ };
975
+ }
976
+ async function handleNextAppRouterContentRefresh(request, config) {
977
+ if (request.method !== "POST") {
978
+ return Response.json({ error: "Method not allowed" }, { status: 405 });
979
+ }
980
+ let body = {};
981
+ try {
982
+ body = await request.json();
983
+ } catch {
984
+ return Response.json({ error: "Invalid JSON body" }, { status: 400 });
985
+ }
986
+ const { status, body: payload } = await runContentRefresh(
987
+ config,
988
+ request.headers.get("authorization") ?? void 0,
989
+ body
990
+ );
991
+ return Response.json(payload, { status });
992
+ }
788
993
  export {
994
+ ContentRefreshAuthError,
789
995
  ContentStore,
790
996
  ContentStoreSDK,
997
+ assertContentRefreshBasicAuth,
998
+ createNextPagesApiContentRefreshHandler,
999
+ encodeBasicAuthCredentials,
1000
+ executeContentRefresh,
791
1001
  fetchCmsBundles,
792
1002
  fetchMergedTranslationBundles,
793
1003
  fetchTranslationBundles,
794
1004
  getDefaultS3RetryConfig,
1005
+ handleNextAppRouterContentRefresh,
1006
+ postContentRefresh,
795
1007
  queryCmsBundle,
796
1008
  trimDepth
797
1009
  };