@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.
- package/dist/mobile-config.d.ts +5 -0
- package/dist/mobile-release/release-order.d.ts +17 -0
- package/dist/mobile-release/release-order.js +16 -0
- package/dist/mobile-release/release-statement.js +24 -0
- package/dist/mobile-release/release.d.ts +12 -5
- package/dist/mobile-release/release.js +22 -8
- package/dist/mobile-release.d.ts +3 -2
- package/dist/mobile-release.js +2 -1
- package/dist/package.js +1 -1
- package/package.json +4 -4
package/dist/mobile-config.d.ts
CHANGED
|
@@ -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
|
|
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 `
|
|
30
|
-
* its
|
|
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[],
|
|
39
|
+
export declare const composeRelease: (files: ReleaseFile[], signing?: ReleaseSigning) => MobileRelease;
|
|
33
40
|
//#endregion
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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 `
|
|
18
|
-
* its
|
|
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,
|
|
21
|
-
if (files.some((file) => file.href ===
|
|
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
|
-
...
|
|
36
|
+
...signing && { signature: signature(content, signing.key) }
|
|
27
37
|
});
|
|
28
38
|
return {
|
|
29
|
-
bundleId: createHash("sha256").update(manifest).update(signer(
|
|
30
|
-
files: new Map([
|
|
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
|
package/dist/mobile-release.d.ts
CHANGED
|
@@ -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 };
|
package/dist/mobile-release.js
CHANGED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.57.
|
|
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.
|
|
45
|
-
"@protobase/query": "0.57.
|
|
46
|
-
"@protobase/schema": "0.57.
|
|
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",
|