@hot-updater/plugin-remote-config 1.0.0-rc.41 → 1.0.0-rc.42

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,96 @@
1
+ const require_template = require("./template.cjs");
2
+ //#region src/server/admin.ts
3
+ const readBody = async (response) => response.json().catch(() => null);
4
+ const field = (body, name) => typeof body === "object" && body !== null ? Reflect.get(body, name) : void 0;
5
+ /** A published version's record, as a publish or a rollback answers it. */
6
+ const isPublishedVersion = (body) => {
7
+ const version = field(body, "version");
8
+ const updateType = field(body, "updateType");
9
+ return typeof version === "number" && Number.isSafeInteger(version) && version > 0 && (updateType === "PUBLISH" || updateType === "ROLLBACK") && typeof field(body, "createdAtMs") === "number";
10
+ };
11
+ const errorText = (body) => {
12
+ const error = field(body, "error");
13
+ return typeof error === "string" ? error : void 0;
14
+ };
15
+ /**
16
+ * Remote Config over a self-hosted server's admin routes, for tooling that
17
+ * reaches it through `standaloneRepository`. It answers as the API does: an
18
+ * invalid template throws `RemoteConfigValidationError` with the server's
19
+ * issues.
20
+ */
21
+ const createRemoteConfigAdminApi = (fetchAdmin, { unavailable = () => /* @__PURE__ */ new Error("The server runs without remoteConfig(): its admin API serves no Remote Config routes.") } = {}) => {
22
+ const request = async (path, init) => {
23
+ const response = await fetchAdmin(path, init === void 0 ? void 0 : {
24
+ method: init.method,
25
+ headers: { "content-type": "application/json" },
26
+ body: JSON.stringify(init.body)
27
+ });
28
+ const body = await readBody(response);
29
+ if (response.status === 404 && errorText(body) === "Not found") throw unavailable();
30
+ return {
31
+ status: response.status,
32
+ body
33
+ };
34
+ };
35
+ const expectOk = (path, { status, body }) => {
36
+ if (status >= 200 && status < 300) return body;
37
+ throw new Error(errorText(body) ?? `The server answered ${path.split("?")[0]} with ${status}.`);
38
+ };
39
+ const published = (path, result) => {
40
+ const currentVersion = field(result.body, "currentVersion");
41
+ if (result.status === 409 && typeof currentVersion === "number" && Number.isSafeInteger(currentVersion)) return {
42
+ status: "conflict",
43
+ currentVersion
44
+ };
45
+ const issues = field(result.body, "issues");
46
+ if (result.status === 400 && Array.isArray(issues)) throw new require_template.RemoteConfigValidationError(issues);
47
+ const version = expectOk(path, result);
48
+ if (!isPublishedVersion(version)) throw new Error(`The server answered ${path} without the version it published.`);
49
+ return {
50
+ status: "published",
51
+ version
52
+ };
53
+ };
54
+ return {
55
+ getActive: async () => {
56
+ const path = "/remote-config/template";
57
+ return expectOk(path, await request(path));
58
+ },
59
+ listVersions: async ({ limit, cursor } = {}) => {
60
+ const query = new URLSearchParams();
61
+ if (limit !== void 0) query.set("limit", String(limit));
62
+ if (cursor !== void 0) query.set("cursor", cursor);
63
+ const path = `/remote-config/versions${query.size === 0 ? "" : `?${query}`}`;
64
+ return expectOk(path, await request(path));
65
+ },
66
+ getVersion: async (version) => {
67
+ const path = `/remote-config/versions/${version}`;
68
+ const result = await request(path);
69
+ return result.status === 404 ? null : expectOk(path, result);
70
+ },
71
+ publish: async ({ template, baseVersion, description }) => {
72
+ const path = "/remote-config/template";
73
+ return published(path, await request(path, {
74
+ method: "PUT",
75
+ body: {
76
+ template,
77
+ baseVersion,
78
+ description
79
+ }
80
+ }));
81
+ },
82
+ rollback: async ({ version, baseVersion, description }) => {
83
+ const path = `/remote-config/versions/${version}/rollback`;
84
+ const result = await request(path, {
85
+ method: "POST",
86
+ body: {
87
+ baseVersion,
88
+ description
89
+ }
90
+ });
91
+ return result.status === 404 ? { status: "not_found" } : published(path, result);
92
+ }
93
+ };
94
+ };
95
+ //#endregion
96
+ exports.createRemoteConfigAdminApi = createRemoteConfigAdminApi;
@@ -0,0 +1,21 @@
1
+ import { RemoteConfigApi } from "./api.cjs";
2
+ import { RemoteDatabase } from "@hot-updater/plugin-core";
3
+ //#region src/server/admin.d.ts
4
+ /** What tooling edits templates with: `RemoteConfigApi` without the devices' `resolve`. */
5
+ type RemoteConfigAdminApi = Omit<RemoteConfigApi, "resolve">;
6
+ interface RemoteConfigAdminApiOptions {
7
+ /**
8
+ * The error for a server that runs without `remoteConfig()`, whose admin
9
+ * API answers its routes with core's 404.
10
+ */
11
+ readonly unavailable?: () => Error;
12
+ }
13
+ /**
14
+ * Remote Config over a self-hosted server's admin routes, for tooling that
15
+ * reaches it through `standaloneRepository`. It answers as the API does: an
16
+ * invalid template throws `RemoteConfigValidationError` with the server's
17
+ * issues.
18
+ */
19
+ declare const createRemoteConfigAdminApi: (fetchAdmin: RemoteDatabase["fetchAdmin"], { unavailable }?: RemoteConfigAdminApiOptions) => RemoteConfigAdminApi;
20
+ //#endregion
21
+ export { RemoteConfigAdminApi, RemoteConfigAdminApiOptions, createRemoteConfigAdminApi };
@@ -0,0 +1,21 @@
1
+ import { RemoteConfigApi } from "./api.mjs";
2
+ import { RemoteDatabase } from "@hot-updater/plugin-core";
3
+ //#region src/server/admin.d.ts
4
+ /** What tooling edits templates with: `RemoteConfigApi` without the devices' `resolve`. */
5
+ type RemoteConfigAdminApi = Omit<RemoteConfigApi, "resolve">;
6
+ interface RemoteConfigAdminApiOptions {
7
+ /**
8
+ * The error for a server that runs without `remoteConfig()`, whose admin
9
+ * API answers its routes with core's 404.
10
+ */
11
+ readonly unavailable?: () => Error;
12
+ }
13
+ /**
14
+ * Remote Config over a self-hosted server's admin routes, for tooling that
15
+ * reaches it through `standaloneRepository`. It answers as the API does: an
16
+ * invalid template throws `RemoteConfigValidationError` with the server's
17
+ * issues.
18
+ */
19
+ declare const createRemoteConfigAdminApi: (fetchAdmin: RemoteDatabase["fetchAdmin"], { unavailable }?: RemoteConfigAdminApiOptions) => RemoteConfigAdminApi;
20
+ //#endregion
21
+ export { RemoteConfigAdminApi, RemoteConfigAdminApiOptions, createRemoteConfigAdminApi };
@@ -0,0 +1,96 @@
1
+ import { RemoteConfigValidationError } from "./template.mjs";
2
+ //#region src/server/admin.ts
3
+ const readBody = async (response) => response.json().catch(() => null);
4
+ const field = (body, name) => typeof body === "object" && body !== null ? Reflect.get(body, name) : void 0;
5
+ /** A published version's record, as a publish or a rollback answers it. */
6
+ const isPublishedVersion = (body) => {
7
+ const version = field(body, "version");
8
+ const updateType = field(body, "updateType");
9
+ return typeof version === "number" && Number.isSafeInteger(version) && version > 0 && (updateType === "PUBLISH" || updateType === "ROLLBACK") && typeof field(body, "createdAtMs") === "number";
10
+ };
11
+ const errorText = (body) => {
12
+ const error = field(body, "error");
13
+ return typeof error === "string" ? error : void 0;
14
+ };
15
+ /**
16
+ * Remote Config over a self-hosted server's admin routes, for tooling that
17
+ * reaches it through `standaloneRepository`. It answers as the API does: an
18
+ * invalid template throws `RemoteConfigValidationError` with the server's
19
+ * issues.
20
+ */
21
+ const createRemoteConfigAdminApi = (fetchAdmin, { unavailable = () => /* @__PURE__ */ new Error("The server runs without remoteConfig(): its admin API serves no Remote Config routes.") } = {}) => {
22
+ const request = async (path, init) => {
23
+ const response = await fetchAdmin(path, init === void 0 ? void 0 : {
24
+ method: init.method,
25
+ headers: { "content-type": "application/json" },
26
+ body: JSON.stringify(init.body)
27
+ });
28
+ const body = await readBody(response);
29
+ if (response.status === 404 && errorText(body) === "Not found") throw unavailable();
30
+ return {
31
+ status: response.status,
32
+ body
33
+ };
34
+ };
35
+ const expectOk = (path, { status, body }) => {
36
+ if (status >= 200 && status < 300) return body;
37
+ throw new Error(errorText(body) ?? `The server answered ${path.split("?")[0]} with ${status}.`);
38
+ };
39
+ const published = (path, result) => {
40
+ const currentVersion = field(result.body, "currentVersion");
41
+ if (result.status === 409 && typeof currentVersion === "number" && Number.isSafeInteger(currentVersion)) return {
42
+ status: "conflict",
43
+ currentVersion
44
+ };
45
+ const issues = field(result.body, "issues");
46
+ if (result.status === 400 && Array.isArray(issues)) throw new RemoteConfigValidationError(issues);
47
+ const version = expectOk(path, result);
48
+ if (!isPublishedVersion(version)) throw new Error(`The server answered ${path} without the version it published.`);
49
+ return {
50
+ status: "published",
51
+ version
52
+ };
53
+ };
54
+ return {
55
+ getActive: async () => {
56
+ const path = "/remote-config/template";
57
+ return expectOk(path, await request(path));
58
+ },
59
+ listVersions: async ({ limit, cursor } = {}) => {
60
+ const query = new URLSearchParams();
61
+ if (limit !== void 0) query.set("limit", String(limit));
62
+ if (cursor !== void 0) query.set("cursor", cursor);
63
+ const path = `/remote-config/versions${query.size === 0 ? "" : `?${query}`}`;
64
+ return expectOk(path, await request(path));
65
+ },
66
+ getVersion: async (version) => {
67
+ const path = `/remote-config/versions/${version}`;
68
+ const result = await request(path);
69
+ return result.status === 404 ? null : expectOk(path, result);
70
+ },
71
+ publish: async ({ template, baseVersion, description }) => {
72
+ const path = "/remote-config/template";
73
+ return published(path, await request(path, {
74
+ method: "PUT",
75
+ body: {
76
+ template,
77
+ baseVersion,
78
+ description
79
+ }
80
+ }));
81
+ },
82
+ rollback: async ({ version, baseVersion, description }) => {
83
+ const path = `/remote-config/versions/${version}/rollback`;
84
+ const result = await request(path, {
85
+ method: "POST",
86
+ body: {
87
+ baseVersion,
88
+ description
89
+ }
90
+ });
91
+ return result.status === 404 ? { status: "not_found" } : published(path, result);
92
+ }
93
+ };
94
+ };
95
+ //#endregion
96
+ export { createRemoteConfigAdminApi };
@@ -4,6 +4,7 @@ const require_api = require("./api.cjs");
4
4
  const require_official = require("./official.cjs");
5
5
  const require_routes = require("./routes.cjs");
6
6
  const require_schema = require("./schema.cjs");
7
+ const require_admin = require("./admin.cjs");
7
8
  let _hot_updater_plugin_core = require("@hot-updater/plugin-core");
8
9
  //#region src/server/index.ts
9
10
  /**
@@ -39,6 +40,7 @@ exports.REMOTE_CONFIG_MAX_PARAMETERS = require_template.REMOTE_CONFIG_MAX_PARAME
39
40
  exports.REMOTE_CONFIG_MAX_TEMPLATE_LENGTH = require_template.REMOTE_CONFIG_MAX_TEMPLATE_LENGTH;
40
41
  exports.RemoteConfigInputError = require_api.RemoteConfigInputError;
41
42
  exports.RemoteConfigValidationError = require_template.RemoteConfigValidationError;
43
+ exports.createRemoteConfigAdminApi = require_admin.createRemoteConfigAdminApi;
42
44
  exports.evaluateRemoteConfig = require_template.evaluateRemoteConfig;
43
45
  exports.matchesRemoteConfigCondition = require_template.matchesRemoteConfigCondition;
44
46
  exports.matchesRemoteConfigRule = require_template.matchesRemoteConfigRule;
@@ -2,6 +2,7 @@ import { RemoteConfigDeviceContext, RemoteConfigFetchResponse } from "../shared/
2
2
  import { RemoteConfigSchema, remoteConfigSchema } from "./schema.cjs";
3
3
  import { EMPTY_REMOTE_CONFIG_TEMPLATE, REMOTE_CONFIG_MAX_CONDITIONS, REMOTE_CONFIG_MAX_PARAMETERS, REMOTE_CONFIG_MAX_TEMPLATE_LENGTH, RemoteConfigCondition, RemoteConfigEvaluatedParameter, RemoteConfigEvaluationContext, RemoteConfigParameter, RemoteConfigParameterValue, RemoteConfigRule, RemoteConfigRuleType, RemoteConfigTemplate, RemoteConfigTemplateIssue, RemoteConfigValidationError, RemoteConfigValueType, evaluateRemoteConfig, matchesRemoteConfigCondition, matchesRemoteConfigRule, resolveRemoteConfigValues, validateRemoteConfigTemplate } from "./template.cjs";
4
4
  import { RemoteConfigActive, RemoteConfigApi, RemoteConfigInputError, RemoteConfigPublishResult, RemoteConfigVersion, RemoteConfigVersionDetail, RemoteConfigVersionsPage } from "./api.cjs";
5
+ import { RemoteConfigAdminApi, RemoteConfigAdminApiOptions, createRemoteConfigAdminApi } from "./admin.cjs";
5
6
  //#region src/server/index.d.ts
6
7
  /**
7
8
  * Remote Config: parameters whose values the server picks per device by
@@ -60,4 +61,4 @@ declare const remoteConfig: () => import("@hot-updater/plugin-core").HotUpdaterP
60
61
  endpoints: readonly import("@hot-updater/plugin-core").PluginEndpoint[];
61
62
  }, {}>;
62
63
  //#endregion
63
- export { EMPTY_REMOTE_CONFIG_TEMPLATE, REMOTE_CONFIG_MAX_CONDITIONS, REMOTE_CONFIG_MAX_PARAMETERS, REMOTE_CONFIG_MAX_TEMPLATE_LENGTH, type RemoteConfigActive, type RemoteConfigApi, type RemoteConfigCondition, type RemoteConfigDeviceContext, type RemoteConfigEvaluatedParameter, type RemoteConfigEvaluationContext, type RemoteConfigFetchResponse, RemoteConfigInputError, type RemoteConfigParameter, type RemoteConfigParameterValue, type RemoteConfigPublishResult, type RemoteConfigRule, type RemoteConfigRuleType, type RemoteConfigSchema, type RemoteConfigTemplate, type RemoteConfigTemplateIssue, RemoteConfigValidationError, type RemoteConfigValueType, type RemoteConfigVersion, type RemoteConfigVersionDetail, type RemoteConfigVersionsPage, evaluateRemoteConfig, matchesRemoteConfigCondition, matchesRemoteConfigRule, remoteConfig, remoteConfigSchema, resolveRemoteConfigValues, validateRemoteConfigTemplate };
64
+ export { EMPTY_REMOTE_CONFIG_TEMPLATE, REMOTE_CONFIG_MAX_CONDITIONS, REMOTE_CONFIG_MAX_PARAMETERS, REMOTE_CONFIG_MAX_TEMPLATE_LENGTH, type RemoteConfigActive, type RemoteConfigAdminApi, type RemoteConfigAdminApiOptions, type RemoteConfigApi, type RemoteConfigCondition, type RemoteConfigDeviceContext, type RemoteConfigEvaluatedParameter, type RemoteConfigEvaluationContext, type RemoteConfigFetchResponse, RemoteConfigInputError, type RemoteConfigParameter, type RemoteConfigParameterValue, type RemoteConfigPublishResult, type RemoteConfigRule, type RemoteConfigRuleType, type RemoteConfigSchema, type RemoteConfigTemplate, type RemoteConfigTemplateIssue, RemoteConfigValidationError, type RemoteConfigValueType, type RemoteConfigVersion, type RemoteConfigVersionDetail, type RemoteConfigVersionsPage, createRemoteConfigAdminApi, evaluateRemoteConfig, matchesRemoteConfigCondition, matchesRemoteConfigRule, remoteConfig, remoteConfigSchema, resolveRemoteConfigValues, validateRemoteConfigTemplate };
@@ -2,6 +2,7 @@ import { RemoteConfigDeviceContext, RemoteConfigFetchResponse } from "../shared/
2
2
  import { RemoteConfigSchema, remoteConfigSchema } from "./schema.mjs";
3
3
  import { EMPTY_REMOTE_CONFIG_TEMPLATE, REMOTE_CONFIG_MAX_CONDITIONS, REMOTE_CONFIG_MAX_PARAMETERS, REMOTE_CONFIG_MAX_TEMPLATE_LENGTH, RemoteConfigCondition, RemoteConfigEvaluatedParameter, RemoteConfigEvaluationContext, RemoteConfigParameter, RemoteConfigParameterValue, RemoteConfigRule, RemoteConfigRuleType, RemoteConfigTemplate, RemoteConfigTemplateIssue, RemoteConfigValidationError, RemoteConfigValueType, evaluateRemoteConfig, matchesRemoteConfigCondition, matchesRemoteConfigRule, resolveRemoteConfigValues, validateRemoteConfigTemplate } from "./template.mjs";
4
4
  import { RemoteConfigActive, RemoteConfigApi, RemoteConfigInputError, RemoteConfigPublishResult, RemoteConfigVersion, RemoteConfigVersionDetail, RemoteConfigVersionsPage } from "./api.mjs";
5
+ import { RemoteConfigAdminApi, RemoteConfigAdminApiOptions, createRemoteConfigAdminApi } from "./admin.mjs";
5
6
  //#region src/server/index.d.ts
6
7
  /**
7
8
  * Remote Config: parameters whose values the server picks per device by
@@ -60,4 +61,4 @@ declare const remoteConfig: () => import("@hot-updater/plugin-core").HotUpdaterP
60
61
  endpoints: readonly import("@hot-updater/plugin-core").PluginEndpoint[];
61
62
  }, {}>;
62
63
  //#endregion
63
- export { EMPTY_REMOTE_CONFIG_TEMPLATE, REMOTE_CONFIG_MAX_CONDITIONS, REMOTE_CONFIG_MAX_PARAMETERS, REMOTE_CONFIG_MAX_TEMPLATE_LENGTH, type RemoteConfigActive, type RemoteConfigApi, type RemoteConfigCondition, type RemoteConfigDeviceContext, type RemoteConfigEvaluatedParameter, type RemoteConfigEvaluationContext, type RemoteConfigFetchResponse, RemoteConfigInputError, type RemoteConfigParameter, type RemoteConfigParameterValue, type RemoteConfigPublishResult, type RemoteConfigRule, type RemoteConfigRuleType, type RemoteConfigSchema, type RemoteConfigTemplate, type RemoteConfigTemplateIssue, RemoteConfigValidationError, type RemoteConfigValueType, type RemoteConfigVersion, type RemoteConfigVersionDetail, type RemoteConfigVersionsPage, evaluateRemoteConfig, matchesRemoteConfigCondition, matchesRemoteConfigRule, remoteConfig, remoteConfigSchema, resolveRemoteConfigValues, validateRemoteConfigTemplate };
64
+ export { EMPTY_REMOTE_CONFIG_TEMPLATE, REMOTE_CONFIG_MAX_CONDITIONS, REMOTE_CONFIG_MAX_PARAMETERS, REMOTE_CONFIG_MAX_TEMPLATE_LENGTH, type RemoteConfigActive, type RemoteConfigAdminApi, type RemoteConfigAdminApiOptions, type RemoteConfigApi, type RemoteConfigCondition, type RemoteConfigDeviceContext, type RemoteConfigEvaluatedParameter, type RemoteConfigEvaluationContext, type RemoteConfigFetchResponse, RemoteConfigInputError, type RemoteConfigParameter, type RemoteConfigParameterValue, type RemoteConfigPublishResult, type RemoteConfigRule, type RemoteConfigRuleType, type RemoteConfigSchema, type RemoteConfigTemplate, type RemoteConfigTemplateIssue, RemoteConfigValidationError, type RemoteConfigValueType, type RemoteConfigVersion, type RemoteConfigVersionDetail, type RemoteConfigVersionsPage, createRemoteConfigAdminApi, evaluateRemoteConfig, matchesRemoteConfigCondition, matchesRemoteConfigRule, remoteConfig, remoteConfigSchema, resolveRemoteConfigValues, validateRemoteConfigTemplate };
@@ -3,6 +3,7 @@ import { RemoteConfigInputError, createRemoteConfigApi } from "./api.mjs";
3
3
  import { markOfficial } from "./official.mjs";
4
4
  import { createRemoteConfigEndpoints } from "./routes.mjs";
5
5
  import { remoteConfigSchema } from "./schema.mjs";
6
+ import { createRemoteConfigAdminApi } from "./admin.mjs";
6
7
  import { definePlugin } from "@hot-updater/plugin-core";
7
8
  //#region src/server/index.ts
8
9
  /**
@@ -32,4 +33,4 @@ const remoteConfig = () => {
32
33
  return markOfficial(plugin);
33
34
  };
34
35
  //#endregion
35
- export { EMPTY_REMOTE_CONFIG_TEMPLATE, REMOTE_CONFIG_MAX_CONDITIONS, REMOTE_CONFIG_MAX_PARAMETERS, REMOTE_CONFIG_MAX_TEMPLATE_LENGTH, RemoteConfigInputError, RemoteConfigValidationError, evaluateRemoteConfig, matchesRemoteConfigCondition, matchesRemoteConfigRule, remoteConfig, remoteConfigSchema, resolveRemoteConfigValues, validateRemoteConfigTemplate };
36
+ export { EMPTY_REMOTE_CONFIG_TEMPLATE, REMOTE_CONFIG_MAX_CONDITIONS, REMOTE_CONFIG_MAX_PARAMETERS, REMOTE_CONFIG_MAX_TEMPLATE_LENGTH, RemoteConfigInputError, RemoteConfigValidationError, createRemoteConfigAdminApi, evaluateRemoteConfig, matchesRemoteConfigCondition, matchesRemoteConfigRule, remoteConfig, remoteConfigSchema, resolveRemoteConfigValues, validateRemoteConfigTemplate };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/plugin-remote-config",
3
3
  "type": "module",
4
- "version": "1.0.0-rc.41",
4
+ "version": "1.0.0-rc.42",
5
5
  "description": "Hot Updater's Remote Config plugin: parameters, conditions, and versioned templates on the server, and values the React Native app reads",
6
6
  "exports": {
7
7
  "./client": {
@@ -34,10 +34,10 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "verkit": "0.4.0",
37
- "@hot-updater/protocol": "1.0.0-rc.41"
37
+ "@hot-updater/protocol": "1.0.0-rc.42"
38
38
  },
39
39
  "peerDependencies": {
40
- "@hot-updater/plugin-core": "^1.0.0-rc.41"
40
+ "@hot-updater/plugin-core": "^1.0.0-rc.42"
41
41
  },
42
42
  "peerDependenciesMeta": {
43
43
  "@hot-updater/plugin-core": {
@@ -47,8 +47,8 @@
47
47
  "devDependencies": {
48
48
  "@types/node": "^20",
49
49
  "vitest": "4.1.4",
50
- "@hot-updater/plugin-core": "1.0.0-rc.41",
51
- "@hot-updater/test-utils": "1.0.0-rc.41"
50
+ "@hot-updater/plugin-core": "1.0.0-rc.42",
51
+ "@hot-updater/test-utils": "1.0.0-rc.42"
52
52
  },
53
53
  "nx": {
54
54
  "implicitDependencies": [