@multiplatform.one/gpg 7.18.0 → 7.20.0

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 (46) hide show
  1. package/dist/cjs/GpgProvider.cjs +7 -4
  2. package/dist/cjs/GpgProvider.native.js +7 -4
  3. package/dist/cjs/GpgProvider.native.js.map +1 -1
  4. package/dist/cjs/index.cjs +7 -1
  5. package/dist/cjs/index.native.js +7 -1
  6. package/dist/cjs/index.native.js.map +1 -1
  7. package/dist/cjs/signature.cjs +113 -0
  8. package/dist/cjs/signature.native.js +124 -0
  9. package/dist/cjs/signature.native.js.map +1 -0
  10. package/dist/cjs/signer.cjs +119 -0
  11. package/dist/cjs/signer.native.js +150 -0
  12. package/dist/cjs/signer.native.js.map +1 -0
  13. package/dist/esm/GpgButton.native.js.map +1 -1
  14. package/dist/esm/GpgContext.native.js.map +1 -1
  15. package/dist/esm/GpgIcon.native.js.map +1 -1
  16. package/dist/esm/GpgProvider.mjs +7 -4
  17. package/dist/esm/GpgProvider.mjs.map +1 -1
  18. package/dist/esm/GpgProvider.native.js +7 -4
  19. package/dist/esm/GpgProvider.native.js.map +1 -1
  20. package/dist/esm/SignInWithGpg.native.js.map +1 -1
  21. package/dist/esm/index.mjs +2 -1
  22. package/dist/esm/index.mjs.map +1 -1
  23. package/dist/esm/index.native.js +2 -1
  24. package/dist/esm/index.native.js.map +1 -1
  25. package/dist/esm/screens.native.js.map +1 -1
  26. package/dist/esm/signature.mjs +88 -0
  27. package/dist/esm/signature.mjs.map +1 -0
  28. package/dist/esm/signature.native.js +96 -0
  29. package/dist/esm/signature.native.js.map +1 -0
  30. package/dist/esm/signer.mjs +88 -0
  31. package/dist/esm/signer.mjs.map +1 -0
  32. package/dist/esm/signer.native.js +116 -0
  33. package/dist/esm/signer.native.js.map +1 -0
  34. package/dist/esm/useGpg.native.js.map +1 -1
  35. package/package.json +16 -3
  36. package/src/GpgProvider.tsx +7 -19
  37. package/src/index.ts +10 -1
  38. package/src/signature.ts +120 -0
  39. package/src/signer.ts +142 -0
  40. package/types/GpgProvider.d.ts.map +1 -1
  41. package/types/index.d.ts +1 -0
  42. package/types/index.d.ts.map +1 -1
  43. package/types/signature.d.ts +6 -0
  44. package/types/signature.d.ts.map +1 -0
  45. package/types/signer.d.ts +47 -0
  46. package/types/signer.d.ts.map +1 -0
package/src/signer.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { useSyncExternalStore } from "react";
2
+ import { gpgSignatureCovers } from "./signature";
3
+
4
+ export { gpgSignatureCovers } from "./signature";
5
+
6
+ // The GPG copy/paste signer as a client, shared by login (GpgProvider) and any
7
+ // other signing an app needs. There is no wallet to talk to: signMessage parks a
8
+ // request, the app shows the terminal command, the user pastes the output back
9
+ // and submit() resolves the pending promise. Only react and @noble/hashes are
10
+ // imported, so the `@multiplatform.one/gpg/signer` subpath stays out of the
11
+ // connectkit graph.
12
+
13
+ export const GPG_SIGNATURE_BEGIN = "-----BEGIN PGP SIGNATURE-----";
14
+ export const GPG_PUBLIC_KEY_BEGIN = "-----BEGIN PGP PUBLIC KEY BLOCK-----";
15
+
16
+ /**
17
+ * The terminal command that signs `message`: a detached armored signature plus
18
+ * the exported public key, which is the blob the user pastes back. printf %s adds
19
+ * no newline, so the signed bytes equal `message`. Single quotes are POSIX-escaped
20
+ * so a message cannot break out of the quoting.
21
+ */
22
+ export function gpgSignCommand(message: string): string {
23
+ return `{ printf %s '${message.replace(/'/g, "'\\''")}' | gpg --detach-sign --armor; gpg --armor --export; }`;
24
+ }
25
+
26
+ /** Trim the pasted terminal output and reject it unless it holds both armored blocks. */
27
+ export function assertGpgSignature(blob: string): string {
28
+ const trimmed = blob.trim();
29
+ if (!trimmed.includes(GPG_SIGNATURE_BEGIN) || !trimmed.includes(GPG_PUBLIC_KEY_BEGIN)) {
30
+ throw new Error(
31
+ "Paste the FULL output — it must contain both a PGP SIGNATURE and a PUBLIC KEY block.",
32
+ );
33
+ }
34
+ return trimmed;
35
+ }
36
+
37
+ export interface GpgSignRequest {
38
+ message: string;
39
+ command: string;
40
+ }
41
+
42
+ export interface GpgSigner {
43
+ readonly family: "gpg";
44
+ /** A GPG identity is the key fingerprint, which only arrives with the signature. */
45
+ readonly address: null;
46
+ readonly capabilities: readonly "signMessage"[];
47
+ /** The request waiting for pasted output, or null. */
48
+ readonly pending: GpgSignRequest | null;
49
+ /**
50
+ * Ask the user to sign `message` in their terminal. Resolves with the pasted
51
+ * armored signature followed by the armored public key, the same blob Keycloak's
52
+ * GPG verifier takes. A second call cancels the first.
53
+ */
54
+ signMessage(message: string): Promise<string>;
55
+ /**
56
+ * Hand pasted output to `request`, the pending request the paste box showed.
57
+ * Throws, and keeps the request pending, when `request` is no longer the
58
+ * pending one, when the paste is incomplete, or when its signature was made
59
+ * over a different message.
60
+ */
61
+ submit(blob: string, request: GpgSignRequest): void;
62
+ cancel(reason?: Error): void;
63
+ subscribe(listener: () => void): () => void;
64
+ }
65
+
66
+ export function createGpgSigner(): GpgSigner {
67
+ let pending: GpgSignRequest | null = null;
68
+ let settle: { resolve: (blob: string) => void; reject: (error: Error) => void } | null = null;
69
+ const listeners = new Set<() => void>();
70
+ const emit = () => {
71
+ for (const listener of listeners) listener();
72
+ };
73
+
74
+ const signer: GpgSigner = {
75
+ family: "gpg",
76
+ address: null,
77
+ capabilities: ["signMessage"],
78
+ get pending() {
79
+ return pending;
80
+ },
81
+ signMessage(message) {
82
+ settle?.reject(new Error("GPG signing was superseded by a newer request"));
83
+ return new Promise<string>((resolve, reject) => {
84
+ settle = { resolve, reject };
85
+ pending = { message, command: gpgSignCommand(message) };
86
+ emit();
87
+ });
88
+ },
89
+ submit(blob, request) {
90
+ if (!settle || !pending) throw new Error("No GPG signature was requested");
91
+ if (request !== pending) {
92
+ throw new Error(
93
+ "That GPG request was superseded; run the current command and paste its output",
94
+ );
95
+ }
96
+ const signature = assertGpgSignature(blob);
97
+ if (!gpgSignatureCovers(signature, pending.message)) {
98
+ throw new Error(
99
+ "That signature is over a different message; run the current command and paste its output",
100
+ );
101
+ }
102
+ const { resolve } = settle;
103
+ settle = null;
104
+ pending = null;
105
+ emit();
106
+ resolve(signature);
107
+ },
108
+ cancel(reason) {
109
+ if (!settle) return;
110
+ const { reject } = settle;
111
+ settle = null;
112
+ pending = null;
113
+ emit();
114
+ reject(reason ?? new Error("GPG signing was cancelled"));
115
+ },
116
+ subscribe(listener) {
117
+ listeners.add(listener);
118
+ return () => listeners.delete(listener);
119
+ },
120
+ };
121
+ return signer;
122
+ }
123
+
124
+ /**
125
+ * Subscribe a component to a GPG signer so it re-renders when a request is
126
+ * parked or settled. Omitted, it is the app-wide signer, so the component that
127
+ * asks for a signature and the one that shows the paste box share one request.
128
+ */
129
+ export function useGpgSigner(signer: GpgSigner = defaultSigner()): GpgSigner {
130
+ useSyncExternalStore(
131
+ signer.subscribe,
132
+ () => signer.pending,
133
+ () => signer.pending,
134
+ );
135
+ return signer;
136
+ }
137
+
138
+ let sharedSigner: GpgSigner | null = null;
139
+ function defaultSigner(): GpgSigner {
140
+ sharedSigner ??= createGpgSigner();
141
+ return sharedSigner;
142
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"GpgProvider.d.ts","sourceRoot":"","sources":["../src/GpgProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyC,MAAM,OAAO,CAAC;AAIzE,OAAO,EAAc,SAAS,EAAkB,MAAM,cAAc,CAAC;AAErE,KAAK,KAAK,GAAG,SAAS,GAAG;IACvB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,mBAAmB,eAAe,CAAC;AAkBhD,eAAO,MAAM,WAAW,GAAI,mFAOzB,KAAK,4CAqGP,CAAC"}
1
+ {"version":3,"file":"GpgProvider.d.ts","sourceRoot":"","sources":["../src/GpgProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAyC,MAAM,OAAO,CAAC;AAIzE,OAAO,EAAc,SAAS,EAAkB,MAAM,cAAc,CAAC;AAGrE,KAAK,KAAK,GAAG,SAAS,GAAG;IACvB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,mBAAmB,eAAe,CAAC;AAkBhD,eAAO,MAAM,WAAW,GAAI,mFAOzB,KAAK,4CAwFP,CAAC"}
package/types/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { useGpg } from "./useGpg";
2
+ export { createGpgSigner, useGpgSigner, gpgSignCommand, assertGpgSignature, gpgSignatureCovers, type GpgSigner, type GpgSignRequest, } from "./signer";
2
3
  export { GpgProvider, GPG_NONCE_QUERY_KEY } from "./GpgProvider";
3
4
  export { GpgContext, GpgStatusState } from "./GpgContext";
4
5
  export type { GpgConfig, GpgContextValue } from "./GpgContext";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1D,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EACL,eAAe,EACf,YAAY,EACZ,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,cAAc,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC1D,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * True when every signature in the pasted armored PGP SIGNATURE block was made
3
+ * over `message`'s UTF-8 bytes. Throws on a block it cannot read.
4
+ */
5
+ export declare function gpgSignatureCovers(blob: string, message: string): boolean;
6
+ //# sourceMappingURL=signature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signature.d.ts","sourceRoot":"","sources":["../src/signature.ts"],"names":[],"mappings":"AA2GA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQzE"}
@@ -0,0 +1,47 @@
1
+ export { gpgSignatureCovers } from "./signature";
2
+ export declare const GPG_SIGNATURE_BEGIN = "-----BEGIN PGP SIGNATURE-----";
3
+ export declare const GPG_PUBLIC_KEY_BEGIN = "-----BEGIN PGP PUBLIC KEY BLOCK-----";
4
+ /**
5
+ * The terminal command that signs `message`: a detached armored signature plus
6
+ * the exported public key, which is the blob the user pastes back. printf %s adds
7
+ * no newline, so the signed bytes equal `message`. Single quotes are POSIX-escaped
8
+ * so a message cannot break out of the quoting.
9
+ */
10
+ export declare function gpgSignCommand(message: string): string;
11
+ /** Trim the pasted terminal output and reject it unless it holds both armored blocks. */
12
+ export declare function assertGpgSignature(blob: string): string;
13
+ export interface GpgSignRequest {
14
+ message: string;
15
+ command: string;
16
+ }
17
+ export interface GpgSigner {
18
+ readonly family: "gpg";
19
+ /** A GPG identity is the key fingerprint, which only arrives with the signature. */
20
+ readonly address: null;
21
+ readonly capabilities: readonly "signMessage"[];
22
+ /** The request waiting for pasted output, or null. */
23
+ readonly pending: GpgSignRequest | null;
24
+ /**
25
+ * Ask the user to sign `message` in their terminal. Resolves with the pasted
26
+ * armored signature followed by the armored public key, the same blob Keycloak's
27
+ * GPG verifier takes. A second call cancels the first.
28
+ */
29
+ signMessage(message: string): Promise<string>;
30
+ /**
31
+ * Hand pasted output to `request`, the pending request the paste box showed.
32
+ * Throws, and keeps the request pending, when `request` is no longer the
33
+ * pending one, when the paste is incomplete, or when its signature was made
34
+ * over a different message.
35
+ */
36
+ submit(blob: string, request: GpgSignRequest): void;
37
+ cancel(reason?: Error): void;
38
+ subscribe(listener: () => void): () => void;
39
+ }
40
+ export declare function createGpgSigner(): GpgSigner;
41
+ /**
42
+ * Subscribe a component to a GPG signer so it re-renders when a request is
43
+ * parked or settled. Omitted, it is the app-wide signer, so the component that
44
+ * asks for a signature and the one that shows the paste box share one request.
45
+ */
46
+ export declare function useGpgSigner(signer?: GpgSigner): GpgSigner;
47
+ //# sourceMappingURL=signer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../src/signer.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AASjD,eAAO,MAAM,mBAAmB,kCAAkC,CAAC;AACnE,eAAO,MAAM,oBAAoB,yCAAyC,CAAC;AAE3E;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,yFAAyF;AACzF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQvD;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IACvB,oFAAoF;IACpF,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,SAAS,aAAa,EAAE,CAAC;IAChD,sDAAsD;IACtD,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IACxC;;;;OAIG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;IACpD,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;CAC7C;AAED,wBAAgB,eAAe,IAAI,SAAS,CAwD3C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,SAA2B,GAAG,SAAS,CAO3E"}