@protobase/server 0.57.2 → 0.57.4

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.
@@ -5,5 +5,10 @@ export type MobileConfig = {
5
5
  appId: string;
6
6
  /** Extra native Capacitor plugins the app needs, as npm name → version range, such as `{ '@capacitor/camera': '^8.0.0' }`. */
7
7
  plugins?: Record<string, string>;
8
+ /**
9
+ * The public key, PEM, of the RSA key in `PROTOBASE_MOBILE_SIGNING_KEY`, which `protobase mobile keys`
10
+ * prints: with it the Android app updates itself, taking only releases signed with that key.
11
+ */
12
+ publicKey?: string;
8
13
  };
9
14
  //#endregion
@@ -0,0 +1,17 @@
1
+ //#region src/mobile-release/release-order.d.ts
2
+ /** What a release's order is computed from: when its bundle was built, and the shell served in place of the bundle's copy. */
3
+ export type ReleaseOrderInput = {
4
+ builtAt?: number;
5
+ shell?: {
6
+ builtAt?: number;
7
+ };
8
+ };
9
+ /**
10
+ * The order of a release: when its bundle was built, then when the server's own shell it serves instead of the
11
+ * bundle's copy was built, 0 with the copy. `undefined` when the bundle or that shell names no build time. The mobile
12
+ * app's built-in files are a bundle's copy, so their order is `[builtAt, 0]`.
13
+ */
14
+ export declare const releaseOrder: ({ builtAt, shell }: ReleaseOrderInput) => number[] | undefined;
15
+ /** An order as the Android live updater reads it, such as `1800000000000,0`. */
16
+ export declare const formatReleaseOrder: (order: number[]) => string;
17
+ //#endregion
@@ -0,0 +1,16 @@
1
+ //#region src/mobile-release/release-order.ts
2
+ /**
3
+ * The order of a release: when its bundle was built, then when the server's own shell it serves instead of the
4
+ * bundle's copy was built, 0 with the copy. `undefined` when the bundle or that shell names no build time. The mobile
5
+ * app's built-in files are a bundle's copy, so their order is `[builtAt, 0]`.
6
+ */
7
+ const releaseOrder = ({ builtAt, shell }) => {
8
+ if (builtAt === void 0) return;
9
+ if (!shell) return [builtAt, 0];
10
+ if (shell.builtAt === void 0) return;
11
+ return [builtAt, shell.builtAt];
12
+ };
13
+ /** An order as the Android live updater reads it, such as `1800000000000,0`. */
14
+ const formatReleaseOrder = (order) => order.join(",");
15
+ //#endregion
16
+ export { formatReleaseOrder, releaseOrder };
@@ -0,0 +1,24 @@
1
+ import { releaseStatementContext } from "@protobase/schema/internal";
2
+ import { sign } from "node:crypto";
3
+ //#region src/mobile-release/release-statement.ts
4
+ /**
5
+ * The statement file of a release bound to `binding`, signed with `signingKey` (see `releaseStatementFile`). The
6
+ * device verifies the signature over the payload's decoded bytes as they are, then parses those same bytes.
7
+ */
8
+ const releaseStatement = (binding, signingKey) => {
9
+ const { manifest, order, appId, fingerprint, shellVersion } = binding;
10
+ const payload = Buffer.from(JSON.stringify({
11
+ manifest,
12
+ order,
13
+ appId,
14
+ fingerprint,
15
+ shellVersion
16
+ }));
17
+ const signature = sign("sha256", Buffer.concat([Buffer.from(releaseStatementContext), payload]), signingKey);
18
+ return new TextEncoder().encode(JSON.stringify({
19
+ payload: payload.toString("hex"),
20
+ signature: signature.toString("hex")
21
+ }));
22
+ };
23
+ //#endregion
24
+ export { releaseStatement };
@@ -1,3 +1,4 @@
1
+ import { ReleaseBinding } from "@protobase/schema/internal";
1
2
  import { KeyObject } from "node:crypto";
2
3
  //#region src/mobile-release/release.d.ts
3
4
  /** A file of a release: its path inside the app's files, such as `assets/index-abc123.js`, and its bytes. */
@@ -12,13 +13,18 @@ export type ServedReleaseFile = {
12
13
  signature?: string;
13
14
  };
14
15
  /**
15
- * A release of the app's files: its bundle id, a hash of its manifest and of the signing key's public key, and every
16
- * file by `href`, the manifest included.
16
+ * A release of the app's files: its bundle id, a hash of what binds it and of the signing key's public key, and every
17
+ * file by `href`, the manifest and, signed, the statement included.
17
18
  */
18
19
  export type MobileRelease = {
19
20
  bundleId: string;
20
21
  files: Map<string, ServedReleaseFile>;
21
22
  };
23
+ /** How a release is signed: the key, and what its statement binds it to beside the manifest. */
24
+ export type ReleaseSigning = {
25
+ key: KeyObject;
26
+ binding: Omit<ReleaseBinding, 'manifest'>;
27
+ };
22
28
  /** The updater's manifest of `files`: each one's `href`, sha256 in hex and size, in the order of their hrefs. */
23
29
  export declare const releaseManifest: (files: ReleaseFile[]) => {
24
30
  href: string;
@@ -26,8 +32,9 @@ export declare const releaseManifest: (files: ReleaseFile[]) => {
26
32
  sizeInBytes: number;
27
33
  }[];
28
34
  /**
29
- * The release of `files`, each hashed and, with `signingKey`, signed once here. Its manifest is served as a file of
30
- * its own, so a file of the app may not have the manifest's name.
35
+ * The release of `files`, each hashed and, with `signing`, signed once here. Its manifest is served as a file of its
36
+ * own and, signed, so is its statement, so a file of the app may not have their names. Signed, the id hashes the
37
+ * statement, which holds the manifest's hash, so the same files released in another order get another id.
31
38
  */
32
- export declare const composeRelease: (files: ReleaseFile[], signingKey?: KeyObject) => MobileRelease;
39
+ export declare const composeRelease: (files: ReleaseFile[], signing?: ReleaseSigning) => MobileRelease;
33
40
  //#endregion
@@ -1,4 +1,5 @@
1
- import { releaseManifestFile } from "@protobase/schema/internal";
1
+ import { releaseStatement } from "./release-statement.js";
2
+ import { releaseManifestFile, releaseStatementContext, releaseStatementFile } from "@protobase/schema/internal";
2
3
  import { createHash, createPublicKey, sign } from "node:crypto";
3
4
  //#region src/mobile-release/release.ts
4
5
  const sha256 = (content) => createHash("sha256").update(content).digest("hex");
@@ -13,21 +14,34 @@ const signer = (signingKey) => signingKey ? createPublicKey(signingKey).export({
13
14
  type: "spki",
14
15
  format: "der"
15
16
  }) : /* @__PURE__ */ new Uint8Array();
17
+ const context = new TextEncoder().encode(releaseStatementContext);
18
+ const startsWithContext = (content) => content.byteLength >= context.byteLength && context.every((byte, i) => content[i] === byte);
16
19
  /**
17
- * The release of `files`, each hashed and, with `signingKey`, signed once here. Its manifest is served as a file of
18
- * its own, so a file of the app may not have the manifest's name.
20
+ * The release of `files`, each hashed and, with `signing`, signed once here. Its manifest is served as a file of its
21
+ * own and, signed, so is its statement, so a file of the app may not have their names. Signed, the id hashes the
22
+ * statement, which holds the manifest's hash, so the same files released in another order get another id.
19
23
  */
20
- const composeRelease = (files, signingKey) => {
21
- if (files.some((file) => file.href === releaseManifestFile)) throw new Error(`The app's files have a ${releaseManifestFile}, the name of the updater's manifest: rename it`);
24
+ const composeRelease = (files, signing) => {
25
+ for (const name of [releaseManifestFile, releaseStatementFile]) if (files.some((file) => file.href === name)) throw new Error(`The app's files have a ${name}, the name of a file of the updater's: rename it`);
26
+ const signed = files.find((file) => startsWithContext(file.content));
27
+ if (signed) throw new Error(`The app's file ${signed.href} starts as a release statement's signed bytes do: change its first line`);
22
28
  const manifest = new TextEncoder().encode(JSON.stringify(releaseManifest(files)));
29
+ const statement = signing && releaseStatement({
30
+ manifest: sha256(manifest),
31
+ ...signing.binding
32
+ }, signing.key);
23
33
  const served = (content) => ({
24
34
  content,
25
35
  checksum: sha256(content),
26
- ...signingKey && { signature: signature(content, signingKey) }
36
+ ...signing && { signature: signature(content, signing.key) }
27
37
  });
28
38
  return {
29
- bundleId: createHash("sha256").update(manifest).update(signer(signingKey)).digest("hex").slice(0, 16),
30
- files: new Map([...files.map(({ href, content }) => [href, served(content)]), [releaseManifestFile, served(manifest)]])
39
+ bundleId: createHash("sha256").update(statement ?? manifest).update(signer(signing?.key)).digest("hex").slice(0, 16),
40
+ files: new Map([
41
+ ...files.map(({ href, content }) => [href, served(content)]),
42
+ [releaseManifestFile, served(manifest)],
43
+ ...statement ? [[releaseStatementFile, served(statement)]] : []
44
+ ])
31
45
  };
32
46
  };
33
47
  //#endregion
@@ -1,6 +1,7 @@
1
- import { MobileRelease, ReleaseFile, ServedReleaseFile, composeRelease, releaseManifest } from "./mobile-release/release.js";
1
+ import { MobileRelease, ReleaseFile, ReleaseSigning, ServedReleaseFile, composeRelease, releaseManifest } from "./mobile-release/release.js";
2
2
  import { MobileReleaseAppInput, OfferedRelease, mobileReleaseApp } from "./mobile-release/release-app.js";
3
3
  import { mobileSigningKeyVariable, readSigningKey } from "./mobile-release/signing-key.js";
4
4
  import { nativeFingerprint } from "./mobile-release/fingerprint.js";
5
+ import { ReleaseOrderInput, formatReleaseOrder, releaseOrder } from "./mobile-release/release-order.js";
5
6
  import { withApiOrigin } from "./mobile-release/api-origin-page.js";
6
- export { type MobileRelease, type MobileReleaseAppInput, type OfferedRelease, type ReleaseFile, type ServedReleaseFile, composeRelease, mobileReleaseApp, mobileSigningKeyVariable, nativeFingerprint, readSigningKey, releaseManifest, withApiOrigin };
7
+ export { type MobileRelease, type MobileReleaseAppInput, type OfferedRelease, type ReleaseFile, type ReleaseOrderInput, type ReleaseSigning, type ServedReleaseFile, composeRelease, formatReleaseOrder, mobileReleaseApp, mobileSigningKeyVariable, nativeFingerprint, readSigningKey, releaseManifest, releaseOrder, withApiOrigin };
@@ -2,5 +2,6 @@ import { composeRelease, releaseManifest } from "./mobile-release/release.js";
2
2
  import { mobileReleaseApp } from "./mobile-release/release-app.js";
3
3
  import { mobileSigningKeyVariable, readSigningKey } from "./mobile-release/signing-key.js";
4
4
  import { nativeFingerprint } from "./mobile-release/fingerprint.js";
5
+ import { formatReleaseOrder, releaseOrder } from "./mobile-release/release-order.js";
5
6
  import { withApiOrigin } from "./mobile-release/api-origin-page.js";
6
- export { composeRelease, mobileReleaseApp, mobileSigningKeyVariable, nativeFingerprint, readSigningKey, releaseManifest, withApiOrigin };
7
+ export { composeRelease, formatReleaseOrder, mobileReleaseApp, mobileSigningKeyVariable, nativeFingerprint, readSigningKey, releaseManifest, releaseOrder, withApiOrigin };
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.57.2";
2
+ var version = "0.57.4";
3
3
  //#endregion
4
4
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/server",
3
- "version": "0.57.2",
3
+ "version": "0.57.4",
4
4
  "description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -41,9 +41,9 @@
41
41
  "@better-auth/passkey": "^1.7.12",
42
42
  "@better-auth/sso": "^1.7.12",
43
43
  "@modelcontextprotocol/server": "^2.3.1",
44
- "@protobase/layout": "0.57.2",
45
- "@protobase/query": "0.57.2",
46
- "@protobase/schema": "0.57.2",
44
+ "@protobase/layout": "0.57.4",
45
+ "@protobase/query": "0.57.4",
46
+ "@protobase/schema": "0.57.4",
47
47
  "@react-email/render": "^2.1.0",
48
48
  "@scalar/hono-api-reference": "^0.12.10",
49
49
  "@sentry/core": "^11.7.0",