hot-updater 1.0.1 → 1.0.2

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.
Files changed (57) hide show
  1. package/dist/index.mjs +26 -1
  2. package/dist/infra-templates/aws/COMMON.md +3 -1
  3. package/dist/infra-templates/aws/app/provision-client-credential.mjs +5 -3
  4. package/dist/infra-templates/aws/lambda/index.cjs +1 -1
  5. package/dist/infra-templates/aws/template.json +10 -10
  6. package/dist/infra-templates/aws/upgrades/README.md +4 -0
  7. package/dist/infra-templates/azure/COMMON.md +310 -0
  8. package/dist/infra-templates/azure/ENVIRONMENT.md +35 -0
  9. package/dist/infra-templates/azure/SETUP.md +163 -0
  10. package/dist/infra-templates/azure/app/hot-updater.config.bare.ts +22 -0
  11. package/dist/infra-templates/azure/app/hot-updater.config.expo.ts +22 -0
  12. package/dist/infra-templates/azure/app/hot-updater.config.rock.ts +22 -0
  13. package/dist/infra-templates/azure/app/hotUpdater.ts +20 -0
  14. package/dist/infra-templates/azure/app/migrate.ts +17 -0
  15. package/dist/infra-templates/azure/app/provision-client-credential.mjs +89 -0
  16. package/dist/infra-templates/azure/app/verify-server.mjs +381 -0
  17. package/dist/infra-templates/azure/azure/function/host.json +8 -0
  18. package/dist/infra-templates/azure/azure/function/index.mjs +70656 -0
  19. package/dist/infra-templates/azure/azure/function/package.json +6 -0
  20. package/dist/infra-templates/azure/template.json +83 -0
  21. package/dist/infra-templates/azure/upgrades/1.0.0.md +215 -0
  22. package/dist/infra-templates/azure/upgrades/README.md +13 -0
  23. package/dist/infra-templates/cloudflare/COMMON.md +3 -1
  24. package/dist/infra-templates/cloudflare/app/provision-client-credential.mjs +5 -3
  25. package/dist/infra-templates/cloudflare/template.json +10 -10
  26. package/dist/infra-templates/cloudflare/upgrades/README.md +4 -0
  27. package/dist/infra-templates/cloudflare/worker/dist/README.md +1 -1
  28. package/dist/infra-templates/cloudflare/worker/dist/index.js +1 -1
  29. package/dist/infra-templates/cloudflare/worker/dist/index.js.map +1 -1
  30. package/dist/infra-templates/firebase/COMMON.md +3 -1
  31. package/dist/infra-templates/firebase/app/migrate.ts +3 -2
  32. package/dist/infra-templates/firebase/app/provision-client-credential.mjs +5 -3
  33. package/dist/infra-templates/firebase/firebase/functions/index.cjs +1 -1
  34. package/dist/infra-templates/firebase/template.json +11 -11
  35. package/dist/infra-templates/firebase/upgrades/README.md +4 -0
  36. package/dist/infra-templates/supabase/COMMON.md +3 -1
  37. package/dist/infra-templates/supabase/app/provision-client-credential.mjs +5 -3
  38. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/package.cjs +1 -1
  39. package/dist/infra-templates/supabase/supabase/functions/hot-updater-v1/_hot-updater/hot-updater-server/dist/package.mjs +1 -1
  40. package/dist/infra-templates/supabase/template.json +10 -10
  41. package/dist/infra-templates/supabase/upgrades/README.md +4 -0
  42. package/dist/infra-templates/vercel/COMMON.md +310 -0
  43. package/dist/infra-templates/vercel/ENVIRONMENT.md +33 -0
  44. package/dist/infra-templates/vercel/SETUP.md +98 -0
  45. package/dist/infra-templates/vercel/app/hot-updater.config.bare.ts +26 -0
  46. package/dist/infra-templates/vercel/app/hot-updater.config.expo.ts +26 -0
  47. package/dist/infra-templates/vercel/app/hot-updater.config.rock.ts +26 -0
  48. package/dist/infra-templates/vercel/app/hotUpdater.ts +24 -0
  49. package/dist/infra-templates/vercel/app/migrate.ts +17 -0
  50. package/dist/infra-templates/vercel/app/provision-client-credential.mjs +89 -0
  51. package/dist/infra-templates/vercel/app/verify-server.mjs +381 -0
  52. package/dist/infra-templates/vercel/template.json +89 -0
  53. package/dist/infra-templates/vercel/upgrades/1.0.0.md +215 -0
  54. package/dist/infra-templates/vercel/upgrades/README.md +13 -0
  55. package/dist/infra-templates/vercel/vercel/api/index.mjs +53589 -0
  56. package/dist/infra-templates/vercel/vercel/vercel.json +14 -0
  57. package/package.json +30 -20
@@ -0,0 +1,163 @@
1
+ # Azure setup checklist
2
+
3
+ Read COMMON.md first. Follow these steps in order and record verified observations
4
+ in deployment.json. Use the Azure CLI (`az`) or other authorized Azure tools; paths
5
+ below are relative to this scaffold. The azure/function/ directory holds the same
6
+ bundled Function package as interactive init: `index.mjs`, `host.json` and
7
+ `package.json`, which need no install. Verify `az version` (Azure CLI 2.60.0 or
8
+ later, for the Flex Consumption commands) and an authenticated
9
+ `az account show` before remote provisioning.
10
+
11
+ ## Upgrade or reconcile an existing deployment
12
+
13
+ Read upgrades/README.md and the Azure section of each applicable version file
14
+ first; a listed change applies before anything below. Then reconcile the
15
+ recorded resource group, storage account, Cosmos DB account and Function App
16
+ with this scaffold: every step is idempotent and reuses what exists. Repeat
17
+ az.database-credential, whose migration creates only what is missing and
18
+ refuses a container of another storage layout, then az.function, which sets
19
+ the Function App's settings and deploys this scaffold's package, and finish
20
+ with az.complete. Reuse the recorded resources and saved client credential;
21
+ never create replacements to upgrade. `hot-updater init` with the saved inputs
22
+ (`--from-env-file .env.hotupdater`) performs the same checks, migration and
23
+ deployment.
24
+
25
+ ## Setup
26
+
27
+ - [ ] **az.scope — Select the subscription and resource group**
28
+ - Inputs: authenticated account, workspace configuration and established region.
29
+ - Run: list enabled subscriptions (`az account list`) and select the one the
30
+ user names or the only one. Register the resource providers
31
+ Microsoft.Storage, Microsoft.DocumentDB, Microsoft.Web, and
32
+ Microsoft.App (which the CLI requires to create a Flex Consumption app) when
33
+ `az provider show` reports them unregistered, as a new subscription does
34
+ (`az provider register --namespace <namespace> --wait`). List the regions
35
+ that host Flex Consumption
36
+ (`az functionapp list-flexconsumption-locations`): the Function App shares
37
+ the group's region. Query the resource group (`az group show`); reuse it
38
+ only when its region is in that list, or choose a listed region for a new
39
+ one and create it (`az group create`) only after every preflight below
40
+ passed. Stop before creating any resource in a group outside the list. Inspect
41
+ existing Hot Updater resources and the upgrade files before adopting them.
42
+ Preserve unrelated resources.
43
+ - Preflight, before creating the resource group or any storage, Cosmos DB
44
+ or Function App resource, as init does; stop on any failure:
45
+ - Storage account (`az storage account show`): an existing one must be a
46
+ general-purpose `StorageV2` (or `Storage`) account, since the Function
47
+ App's host storage needs Blob, Queue and Table services, must allow
48
+ shared key access, and must not restrict network access
49
+ (`publicNetworkAccess` not `Disabled`, `networkRuleSet.defaultAction`
50
+ not `Deny`): devices download from it, and the setup creates no virtual
51
+ network. A new name must pass `az storage account check-name`.
52
+ - Cosmos DB account (`az cosmosdb show`): an existing one must be API for
53
+ NoSQL (`kind` `GlobalDocumentDB`, no Mongo/Cassandra/Gremlin/Table
54
+ capability), keep key authentication (`disableLocalAuth` and
55
+ `disableKeyBasedMetadataWriteAccess` not true, since the migration
56
+ creates and updates the database and container with the key) and
57
+ write in one region (`enableMultipleWriteLocations` not true), and be
58
+ reachable from the Function App, which joins no virtual network:
59
+ `publicNetworkAccess` not `Disabled`, and with IP or virtual network
60
+ rules, an `0.0.0.0` rule that admits Azure datacenters and a rule that
61
+ admits the machine running the migration (reading the `hot-updater`
62
+ database with the key answers, or 404s, rather than 403). An existing
63
+ `hot_updater_v1` container in the `hot-updater` database must be
64
+ partitioned by `/partition` alone, have no unique keys, and either hold
65
+ a `schema.engine` setting of this version's layout or no items at all
66
+ (`az cosmosdb sql container show`, then a cross-partition
67
+ `SELECT TOP 1 c.id FROM c`). When its default consistency is not
68
+ `Strong`, get the user's approval to change it, since other apps on it
69
+ read under it too, and change it as the last preflight step, before any
70
+ resource is created (`az cosmosdb update --default-consistency-level
71
+ Strong`): Azure refuses it for an account whose regions lie more than
72
+ 5,000 miles apart unless support enabled global strong consistency, and
73
+ then stop. A new name must be free
74
+ (`az cosmosdb check-name-exists` returns false).
75
+ - Function App (`az functionapp show`): an existing one must be a Flex
76
+ Consumption app on Node.js 22 or later (`properties.functionAppConfig.runtime`)
77
+ that is Hot Updater's: its app settings include
78
+ HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING, or it runs no functions yet
79
+ (`az functionapp function list`). Deploying replaces its whole package. A new name must be free across
80
+ azurewebsites.net (`az rest --method post --url
81
+ https://management.azure.com/subscriptions/<subscription-id>/providers/Microsoft.Web/checknameavailability?api-version=2024-04-01
82
+ --body '{"name":"<app>","type":"Microsoft.Web/sites"}'`).
83
+ - Verify/record: subscriptionId, resourceGroup, a location that hosts Flex
84
+ Consumption, and the preflight results.
85
+ - Retry: query the same group before another create request.
86
+
87
+ - [ ] **az.storage — Prepare the storage account**
88
+ - Requires: az.scope.
89
+ - Run: reuse the account az.scope checked, or create a StorageV2, Standard_LRS
90
+ account with TLS 1.2 and blob public access disabled. Shared key access
91
+ must stay allowed: the bundles' SAS URLs and the Function's settings use
92
+ the account key. Create the private `hot-updater` blob container when
93
+ missing, or make an existing one private (no anonymous `publicAccess`):
94
+ devices download bundles through SAS URLs.
95
+ - Verify/record: storageAccount, its region and the container.
96
+ - Retry: query the account and container; never create a second account to
97
+ resolve a naming conflict the user did not approve.
98
+
99
+ - [ ] **az.cosmos — Prepare the Cosmos DB account**
100
+ - Requires: az.scope.
101
+ - Run: reuse the account az.scope checked (and made `Strong` when the user
102
+ approved it there), or create one: API for NoSQL
103
+ (`--kind GlobalDocumentDB`), serverless (`--capabilities EnableServerless`
104
+ on the core CLI; `--capacity-mode Serverless` when `az cosmosdb create
105
+ --help` lists it, as the cosmosdb-preview extension does, whose preview
106
+ API refuses the capability) in
107
+ the group's region, with `Strong` default consistency. Account creation
108
+ takes several minutes.
109
+ - Verify/record: cosmosAccount, API, capacity mode and consistency.
110
+ - Retry: query the account's provisioning state instead of creating it again.
111
+
112
+ - [ ] **az.database-credential — Create the container, write the schema settings and register the client credential**
113
+ - Requires: az.storage and az.cosmos. Complete this before Function deployment, as init does.
114
+ - Run: read the connection strings (`az storage account
115
+ show-connection-string`, `az cosmosdb keys list --type
116
+ connection-strings`) into the app's ignored .env.hotupdater as
117
+ HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING and
118
+ HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING; never paste them into chat.
119
+ When the `hot_updater_v1` container of the `hot-updater` database exists,
120
+ read its settings item (`pk = private_hot_updater_settings`,
121
+ `row.key = schema.engine`); its `row.value` must be the string `"1"` when
122
+ present. A missing marker is acceptable only for an empty container. Stop
123
+ on any other value, a populated container without the marker, a container
124
+ not partitioned by `/partition`, or an unknown layout; preserve the data
125
+ for investigation. Then follow COMMON.md's Local CLI and client credential
126
+ steps. Run app/provision-client-credential.mjs from the app directory: the
127
+ helper loads app/hotUpdater.ts, the credential helper's server definition,
128
+ which stays in the scaffold and is never copied into the app. The helper
129
+ first runs app/migrate.ts, which creates the database and container when
130
+ they are missing and writes the schema settings of core and the plugins
131
+ app/hotUpdater.ts runs (the database answers 503 until they exist),
132
+ then registers the client API key.
133
+ - Verify/record: helper succeeds against the chosen account and the
134
+ API key is persisted privately. Do not create settings items
135
+ by hand.
136
+ - Retry: repeat the compatibility preflight, then rerun the helper with the
137
+ saved API key. Never edit settings to bypass a failed check or
138
+ rotate the API key.
139
+
140
+ - [ ] **az.function — Deploy the server**
141
+ - Requires: az.database-credential.
142
+ - Run: reuse the app az.scope checked, or create it on a Flex Consumption
143
+ plan (`az functionapp create --flexconsumption-location <location>
144
+ --runtime node --runtime-version 22 --storage-account <account>`). Set the
145
+ app settings HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING,
146
+ HOT_UPDATER_AZURE_BLOB_CONTAINER (`hot-updater`) and
147
+ HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING, preserving unrelated settings.
148
+ Zip the three files of azure/function/ at the archive root and deploy them
149
+ (`az functionapp deployment source config-zip --src <zip>`).
150
+ - Verify/record: functionApp, its default host name and baseUrl
151
+ (`https://<defaultHostName>`). host.json drops the `/api` prefix, so the
152
+ host name serves every route.
153
+ - Retry: inspect the deployment status and logs; reuse the same Function App.
154
+
155
+ - [ ] **az.complete — Verify and report**
156
+ - Requires: az.function.
157
+ - Run: check `<baseUrl>/version` and complete common.verify, common.local and
158
+ common.report in COMMON.md.
159
+ - Verify/record: target version/generation, catalog authentication and local
160
+ configuration pass. Resolve/download an existing artifact when available:
161
+ /version and an empty catalog do not exercise SAS download URLs. Report app
162
+ integration/OTA validation separately from server readiness.
163
+ - Retry: inspect the failing check and corresponding step, preserving verified resources.
@@ -0,0 +1,22 @@
1
+ import { azureBlobStorage, cosmosDatabase } from "@hot-updater/azure";
2
+ import { bare } from "@hot-updater/bare";
3
+ import { defineConfig } from "hot-updater";
4
+ import { apiKeys, insights, remoteConfig } from "hot-updater/plugins";
5
+ import { existsSync } from "node:fs";
6
+
7
+ if (existsSync(".env.hotupdater")) {
8
+ process.loadEnvFile(".env.hotupdater");
9
+ }
10
+
11
+ export default defineConfig({
12
+ build: bare({ enableHermes: true }),
13
+ storage: azureBlobStorage({
14
+ connectionString: process.env.HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING!,
15
+ containerName: "hot-updater",
16
+ }),
17
+ database: cosmosDatabase({
18
+ connectionString: process.env.HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING!,
19
+ }),
20
+ plugins: [apiKeys(), insights(), remoteConfig()],
21
+ updateStrategy: "appVersion", // or "fingerprint"
22
+ });
@@ -0,0 +1,22 @@
1
+ import { azureBlobStorage, cosmosDatabase } from "@hot-updater/azure";
2
+ import { expo } from "@hot-updater/expo";
3
+ import { defineConfig } from "hot-updater";
4
+ import { apiKeys, insights, remoteConfig } from "hot-updater/plugins";
5
+ import { existsSync } from "node:fs";
6
+
7
+ if (existsSync(".env.hotupdater")) {
8
+ process.loadEnvFile(".env.hotupdater");
9
+ }
10
+
11
+ export default defineConfig({
12
+ build: expo(),
13
+ storage: azureBlobStorage({
14
+ connectionString: process.env.HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING!,
15
+ containerName: "hot-updater",
16
+ }),
17
+ database: cosmosDatabase({
18
+ connectionString: process.env.HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING!,
19
+ }),
20
+ plugins: [apiKeys(), insights(), remoteConfig()],
21
+ updateStrategy: "appVersion", // or "fingerprint"
22
+ });
@@ -0,0 +1,22 @@
1
+ import { azureBlobStorage, cosmosDatabase } from "@hot-updater/azure";
2
+ import { rock } from "@hot-updater/rock";
3
+ import { defineConfig } from "hot-updater";
4
+ import { apiKeys, insights, remoteConfig } from "hot-updater/plugins";
5
+ import { existsSync } from "node:fs";
6
+
7
+ if (existsSync(".env.hotupdater")) {
8
+ process.loadEnvFile(".env.hotupdater");
9
+ }
10
+
11
+ export default defineConfig({
12
+ build: rock(),
13
+ storage: azureBlobStorage({
14
+ connectionString: process.env.HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING!,
15
+ containerName: "hot-updater",
16
+ }),
17
+ database: cosmosDatabase({
18
+ connectionString: process.env.HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING!,
19
+ }),
20
+ plugins: [apiKeys(), insights(), remoteConfig()],
21
+ updateStrategy: "appVersion", // or "fingerprint"
22
+ });
@@ -0,0 +1,20 @@
1
+ import { azureBlobStorage, cosmosDatabase } from "@hot-updater/azure";
2
+ import { createHotUpdater } from "@hot-updater/server";
3
+ import { apiKeys, insights, remoteConfig } from "@hot-updater/server/plugins";
4
+
5
+ /**
6
+ * The deployed server's database, storage, and plugins, on which
7
+ * provision-client-credential.mjs registers the app's client credential.
8
+ * It stays in the scaffold: the app's hot-updater.config.ts lists the same
9
+ * database, storage, and plugins.
10
+ */
11
+ export const hotUpdater = createHotUpdater({
12
+ database: cosmosDatabase({
13
+ connectionString: process.env.HOT_UPDATER_AZURE_COSMOS_CONNECTION_STRING!,
14
+ }),
15
+ storage: azureBlobStorage({
16
+ connectionString: process.env.HOT_UPDATER_AZURE_STORAGE_CONNECTION_STRING!,
17
+ containerName: "hot-updater",
18
+ }),
19
+ plugins: [apiKeys(), insights(), remoteConfig()],
20
+ });
@@ -0,0 +1,17 @@
1
+ import { toolingTargetOf } from "@hot-updater/plugin-core";
2
+ import type { HotUpdaterAPI } from "@hot-updater/server";
3
+
4
+ /**
5
+ * Creates the tables of core and the plugins `hotUpdater` runs where the
6
+ * database keeps tables, or its store where it has one to prepare, and writes
7
+ * their schema settings, which the database checks before its first read.
8
+ */
9
+ export const migrate = async ({ database, plugins }: HotUpdaterAPI) => {
10
+ if (database.createMigrator === undefined) {
11
+ throw new Error(`The ${database.name} database has no migrator.`);
12
+ }
13
+ const result = await database
14
+ .createMigrator(toolingTargetOf(plugins))
15
+ .migrateToLatest({ updateSettings: true });
16
+ await result.execute();
17
+ };
@@ -0,0 +1,89 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+
4
+ // The credential helper's server definition reads process.env when it loads,
5
+ // so the settings go first; a static import would load it before them.
6
+ if (existsSync(".env.hotupdater")) {
7
+ process.loadEnvFile(".env.hotupdater");
8
+ }
9
+ const { hotUpdater } = await import("./hotUpdater.ts");
10
+ if (
11
+ typeof hotUpdater !== "object" ||
12
+ hotUpdater === null ||
13
+ typeof hotUpdater.database !== "object" ||
14
+ !Array.isArray(hotUpdater.plugins) ||
15
+ typeof hotUpdater.api !== "object"
16
+ ) {
17
+ throw new Error(
18
+ "app/hotUpdater.ts, the credential helper's server definition, must export `hotUpdater`: the server createHotUpdater from @hot-updater/server returns.",
19
+ );
20
+ }
21
+ const { api, clientAuth, database, plugins } = hotUpdater;
22
+
23
+ const credentialPath = new URL("./client-credential.local", import.meta.url);
24
+ // Providers whose schema comes from the server's migrator (Firestore, Cosmos
25
+ // DB, and Vercel's PostgreSQL) ship migrate.ts.
26
+ const migrationPath = new URL("./migrate.ts", import.meta.url);
27
+ try {
28
+ // The plugin that protects the deployed server's client routes, if any,
29
+ // and the credential an app sends it.
30
+ const clientCredential =
31
+ clientAuth === undefined
32
+ ? undefined
33
+ : plugins.find(({ id }) => id === clientAuth.plugin)?.cli
34
+ ?.clientCredential;
35
+ if (
36
+ clientAuth !== undefined &&
37
+ (typeof clientCredential !== "object" ||
38
+ clientCredential === null ||
39
+ typeof clientCredential.label !== "string" ||
40
+ typeof clientCredential.header !== "string" ||
41
+ typeof clientCredential.env !== "string" ||
42
+ typeof clientCredential.generate !== "function" ||
43
+ typeof clientCredential.provision !== "function")
44
+ ) {
45
+ throw new Error(
46
+ `Plugin "${clientAuth.plugin}" provides clientAuth but no cli.clientCredential with a label, header, env, generate, and provision, so the app cannot get its credential.`,
47
+ );
48
+ }
49
+ let credential;
50
+ if (clientCredential !== undefined) {
51
+ const { env, label } = clientCredential;
52
+ credential = await readFile(credentialPath, "utf8").catch((error) => {
53
+ if (error.code === "ENOENT") return undefined;
54
+ throw error;
55
+ });
56
+ const existing = process.env[env]?.trim();
57
+ if (credential && existing && credential.trim() !== existing) {
58
+ throw new Error(
59
+ `Saved client credentials differ. Resolve the target ${label} before provisioning.`,
60
+ );
61
+ }
62
+ if (!credential) {
63
+ credential = existing || clientCredential.generate();
64
+ await writeFile(credentialPath, credential, { flag: "wx", mode: 0o600 });
65
+ }
66
+ }
67
+ if (existsSync(migrationPath)) {
68
+ // It creates the tables of core and the deployed server's plugins where
69
+ // the database keeps tables, and writes their schema settings, which the
70
+ // database checks before its first read.
71
+ const { migrate } = await import(migrationPath.href);
72
+ await migrate(hotUpdater);
73
+ }
74
+ if (clientCredential === undefined) {
75
+ console.log("Client routes are public: there is no client credential.");
76
+ } else {
77
+ // Registered again rather than replaced, through that plugin, on the
78
+ // tables the deployed server reads.
79
+ await clientCredential.provision(api[clientAuth.plugin], {
80
+ existing: credential.trim(),
81
+ name: "Agent infrastructure setup",
82
+ });
83
+ console.log(
84
+ `Client ${clientCredential.label} registered. It is saved in client-credential.local.`,
85
+ );
86
+ }
87
+ } finally {
88
+ await database.dispose?.();
89
+ }