@otakit/capacitor-updater 1.2.0 → 2.0.1
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/README.md +23 -18
- package/android/src/main/java/com/updatekit/updater/{StatsClient.java → DeviceEventClient.java} +30 -19
- package/android/src/main/java/com/updatekit/updater/HostedManifestKeys.java +5 -6
- package/android/src/main/java/com/updatekit/updater/ManifestClient.java +33 -47
- package/android/src/main/java/com/updatekit/updater/ManifestVerifier.java +1 -75
- package/android/src/main/java/com/updatekit/updater/UpdaterPlugin.java +160 -99
- package/dist/esm/definitions.d.ts +9 -25
- package/dist/esm/definitions.d.ts.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +0 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +0 -4
- package/dist/esm/web.d.ts.map +1 -1
- package/dist/esm/web.js +0 -8
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +0 -10
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +0 -10
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/UpdaterPlugin/{StatsClient.swift → DeviceEventClient.swift} +21 -21
- package/ios/Sources/UpdaterPlugin/HostedManifestKeys.swift +5 -5
- package/ios/Sources/UpdaterPlugin/ManifestClient.swift +32 -52
- package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +2 -56
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.m +0 -2
- package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +139 -99
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Capacitor OTA updater plugin for OtaKit.
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- fetches the latest manifest for its release lane from the CDN
|
|
8
8
|
- downloads and verifies OTA bundles
|
|
9
9
|
- stages updates safely
|
|
10
10
|
- activates them on the next launch, next resume, or immediately
|
|
@@ -14,6 +14,11 @@ Capacitor OTA updater plugin for OtaKit.
|
|
|
14
14
|
- requires `notifyAppReady()` as the success handshake
|
|
15
15
|
- rolls back automatically if the new bundle does not prove healthy
|
|
16
16
|
|
|
17
|
+
OtaKit publishes signed static manifests into object storage behind a CDN. The
|
|
18
|
+
plugin fetches the manifest for its `appId + channel + runtimeVersion` lane,
|
|
19
|
+
verifies it, compares it against the current and staged bundle locally, and
|
|
20
|
+
only downloads when the manifest actually points at something newer.
|
|
21
|
+
|
|
17
22
|
For normal app code, the main public methods are:
|
|
18
23
|
|
|
19
24
|
```ts
|
|
@@ -35,9 +40,6 @@ For manual mode, `getState()` tells you if something is already staged,
|
|
|
35
40
|
`check()` tells you whether a newer update exists, `download()` stages it, and
|
|
36
41
|
`update()` is the one-shot helper that downloads and applies the newest update.
|
|
37
42
|
|
|
38
|
-
Manual inspection and support methods like state inspection and reset live under
|
|
39
|
-
`OtaKit.debug`.
|
|
40
|
-
|
|
41
43
|
## Hosted config
|
|
42
44
|
|
|
43
45
|
```ts
|
|
@@ -57,12 +59,14 @@ plugins: {
|
|
|
57
59
|
|
|
58
60
|
Advanced overrides for self-hosting or custom trust only:
|
|
59
61
|
|
|
60
|
-
- `
|
|
62
|
+
- `cdnUrl` for manifest and bundle delivery
|
|
63
|
+
- `ingestUrl` for event ingest requests
|
|
64
|
+
- `serverUrl` for self-hosted control-plane tooling such as the CLI. The native runtime uses `cdnUrl` and `ingestUrl` instead.
|
|
61
65
|
- `manifestKeys`
|
|
62
66
|
- `allowInsecureUrls`
|
|
63
67
|
|
|
64
|
-
Hosted OtaKit already points at
|
|
65
|
-
the managed manifest signing keys.
|
|
68
|
+
Hosted OtaKit already points at the managed ingest service and CDN and already
|
|
69
|
+
trusts the managed manifest signing keys.
|
|
66
70
|
|
|
67
71
|
## Channels vs runtimeVersion
|
|
68
72
|
|
|
@@ -84,11 +88,12 @@ When `runtimeVersion` is set:
|
|
|
84
88
|
|
|
85
89
|
The plugin does not just download from a URL and trust the result.
|
|
86
90
|
|
|
87
|
-
1. it fetches
|
|
91
|
+
1. it fetches the latest manifest from the CDN for its app + channel + runtimeVersion lane
|
|
88
92
|
2. it verifies the manifest signature when manifest keys are configured
|
|
89
|
-
3. it
|
|
90
|
-
4.
|
|
91
|
-
5. it
|
|
93
|
+
3. it compares that manifest against the current and staged bundle already on the device
|
|
94
|
+
4. if the manifest is newer, it downloads the bundle zip
|
|
95
|
+
5. it verifies the zip against the manifest `sha256`
|
|
96
|
+
6. it stages and activates the bundle
|
|
92
97
|
|
|
93
98
|
In the hosted path, managed signing keys are already built in.
|
|
94
99
|
|
|
@@ -114,7 +119,7 @@ resume**.
|
|
|
114
119
|
no automatic checks, no automatic staged activation.
|
|
115
120
|
the app integration drives everything via `check()`, `download()`, `apply()`, or `update()`.
|
|
116
121
|
|
|
117
|
-
###
|
|
122
|
+
### Development mode
|
|
118
123
|
|
|
119
124
|
- `immediate`
|
|
120
125
|
checks, downloads, and activates in one shot as soon as possible on cold start and resume (the user may briefly see the previous version before a reload).
|
|
@@ -165,8 +170,9 @@ await OtaKit.notifyAppReady();
|
|
|
165
170
|
```
|
|
166
171
|
|
|
167
172
|
The plugin handles checking, downloading, activation, and rollback based on
|
|
168
|
-
`updateMode`.
|
|
169
|
-
the background
|
|
173
|
+
`updateMode`. In `next-launch` and `next-resume`, it checks on cold start and
|
|
174
|
+
every time the app comes back from the background, throttled by `checkInterval`.
|
|
175
|
+
`immediate` bypasses that throttle.
|
|
170
176
|
|
|
171
177
|
For most apps, this is the entire runtime integration.
|
|
172
178
|
|
|
@@ -195,10 +201,9 @@ only after explicit user confirmation.
|
|
|
195
201
|
|
|
196
202
|
## Throttle
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
null.
|
|
204
|
+
`checkInterval` (default 10 min) only applies to automatic checks in
|
|
205
|
+
`next-launch` and `next-resume`. Manual `check()` / `download()` calls are
|
|
206
|
+
always live, and `immediate` mode ignores the interval entirely.
|
|
202
207
|
|
|
203
208
|
## Retention and deletion
|
|
204
209
|
|
package/android/src/main/java/com/updatekit/updater/{StatsClient.java → DeviceEventClient.java}
RENAMED
|
@@ -4,52 +4,57 @@ import java.io.OutputStream;
|
|
|
4
4
|
import java.net.HttpURLConnection;
|
|
5
5
|
import java.net.URL;
|
|
6
6
|
import java.nio.charset.StandardCharsets;
|
|
7
|
+
import java.text.SimpleDateFormat;
|
|
8
|
+
import java.util.Date;
|
|
9
|
+
import java.util.Locale;
|
|
10
|
+
import java.util.TimeZone;
|
|
7
11
|
import java.util.concurrent.ExecutorService;
|
|
8
12
|
import java.util.concurrent.Executors;
|
|
13
|
+
import java.util.UUID;
|
|
9
14
|
import org.json.JSONObject;
|
|
10
15
|
|
|
11
|
-
final class
|
|
16
|
+
final class DeviceEventClient {
|
|
12
17
|
|
|
13
18
|
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
14
19
|
|
|
15
|
-
private
|
|
20
|
+
private DeviceEventClient() {}
|
|
16
21
|
|
|
17
22
|
static void send(
|
|
18
|
-
String
|
|
23
|
+
String ingestUrl,
|
|
19
24
|
String appId,
|
|
20
25
|
String platform,
|
|
21
26
|
String action,
|
|
22
27
|
String bundleVersion,
|
|
23
28
|
String channel,
|
|
29
|
+
String runtimeVersion,
|
|
24
30
|
String releaseId,
|
|
25
31
|
String nativeBuild,
|
|
26
|
-
String
|
|
32
|
+
String detail
|
|
27
33
|
) {
|
|
28
34
|
executor.execute(() -> {
|
|
29
35
|
HttpURLConnection connection = null;
|
|
30
36
|
try {
|
|
31
|
-
String base =
|
|
32
|
-
URL url = new URL(base + "/
|
|
37
|
+
String base = ingestUrl.replaceAll("/+$", "");
|
|
38
|
+
URL url = new URL(base + "/events");
|
|
33
39
|
|
|
34
40
|
JSONObject payload = new JSONObject();
|
|
41
|
+
payload.put("eventId", UUID.randomUUID().toString());
|
|
42
|
+
payload.put("sentAt", iso8601Now());
|
|
35
43
|
payload.put("platform", platform);
|
|
36
44
|
payload.put("action", action);
|
|
37
|
-
|
|
38
|
-
payload.put("bundleVersion", bundleVersion);
|
|
39
|
-
}
|
|
45
|
+
payload.put("bundleVersion", bundleVersion);
|
|
40
46
|
if (channel != null && !channel.isEmpty()) {
|
|
41
47
|
payload.put("channel", channel);
|
|
42
48
|
}
|
|
43
|
-
if (
|
|
44
|
-
payload.put("
|
|
45
|
-
}
|
|
46
|
-
if (nativeBuild != null) {
|
|
47
|
-
payload.put("nativeBuild", nativeBuild);
|
|
49
|
+
if (runtimeVersion != null && !runtimeVersion.isEmpty()) {
|
|
50
|
+
payload.put("runtimeVersion", runtimeVersion);
|
|
48
51
|
}
|
|
49
|
-
|
|
52
|
+
payload.put("releaseId", releaseId);
|
|
53
|
+
payload.put("nativeBuild", nativeBuild);
|
|
54
|
+
if (detail != null) {
|
|
50
55
|
String truncated =
|
|
51
|
-
|
|
52
|
-
payload.put("
|
|
56
|
+
detail.length() > 500 ? detail.substring(0, 500) : detail;
|
|
57
|
+
payload.put("detail", truncated);
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
byte[] body = payload.toString().getBytes(StandardCharsets.UTF_8);
|
|
@@ -66,10 +71,10 @@ final class StatsClient {
|
|
|
66
71
|
output.write(body);
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
//
|
|
74
|
+
// Device events are best-effort and should never block the update flow.
|
|
70
75
|
connection.getResponseCode();
|
|
71
76
|
} catch (Exception ignored) {
|
|
72
|
-
//
|
|
77
|
+
// Device events are best-effort, don't fail on errors
|
|
73
78
|
} finally {
|
|
74
79
|
if (connection != null) {
|
|
75
80
|
connection.disconnect();
|
|
@@ -77,4 +82,10 @@ final class StatsClient {
|
|
|
77
82
|
}
|
|
78
83
|
});
|
|
79
84
|
}
|
|
85
|
+
|
|
86
|
+
private static String iso8601Now() {
|
|
87
|
+
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
|
|
88
|
+
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
89
|
+
return formatter.format(new Date());
|
|
90
|
+
}
|
|
80
91
|
}
|
|
@@ -7,18 +7,17 @@ import java.util.Locale;
|
|
|
7
7
|
|
|
8
8
|
final class HostedManifestKeys {
|
|
9
9
|
|
|
10
|
-
private static final String
|
|
10
|
+
private static final String MANAGED_CDN_URL = "https://cdn.otakit.app";
|
|
11
11
|
|
|
12
12
|
private HostedManifestKeys() {}
|
|
13
13
|
|
|
14
|
-
static boolean
|
|
15
|
-
if (
|
|
14
|
+
static boolean matchesManagedManifestUrl(String cdnUrl) {
|
|
15
|
+
if (cdnUrl == null) {
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
String normalized =
|
|
20
|
-
return normalized.equals(
|
|
21
|
-
|| normalized.equals("https://otakit.app/api/v1");
|
|
19
|
+
String normalized = cdnUrl.trim().replaceAll("/+$", "").toLowerCase(Locale.ROOT);
|
|
20
|
+
return normalized.equals(MANAGED_CDN_URL) || normalized.equals("https://www.otakit.app");
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
static List<ManifestVerifier.KeyEntry> createDefaultKeys() {
|
|
@@ -9,6 +9,9 @@ import org.json.JSONObject;
|
|
|
9
9
|
|
|
10
10
|
final class ManifestClient {
|
|
11
11
|
|
|
12
|
+
private static final String BASE_CHANNEL_KEY = "__base__";
|
|
13
|
+
private static final String DEFAULT_RUNTIME_KEY = "__default__";
|
|
14
|
+
|
|
12
15
|
static final class ManifestSignature {
|
|
13
16
|
|
|
14
17
|
final String kid;
|
|
@@ -63,41 +66,33 @@ final class ManifestClient {
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
static LatestManifest fetchLatest(
|
|
66
|
-
String
|
|
69
|
+
String cdnUrl,
|
|
67
70
|
String appId,
|
|
68
71
|
String channel,
|
|
69
|
-
String currentVersion,
|
|
70
|
-
String currentReleaseId,
|
|
71
72
|
String runtimeVersion,
|
|
72
|
-
String platform,
|
|
73
73
|
boolean allowInsecureUrls,
|
|
74
74
|
java.util.List<ManifestVerifier.KeyEntry> manifestKeys
|
|
75
75
|
) throws Exception {
|
|
76
|
-
String base =
|
|
77
|
-
|
|
76
|
+
String base = cdnUrl.replaceAll("/+$", "");
|
|
77
|
+
String channelKey = channel != null && !channel.trim().isEmpty() ? channel.trim() : BASE_CHANNEL_KEY;
|
|
78
|
+
String runtimeKey =
|
|
79
|
+
runtimeVersion != null && !runtimeVersion.trim().isEmpty()
|
|
80
|
+
? runtimeVersion.trim()
|
|
81
|
+
: DEFAULT_RUNTIME_KEY;
|
|
82
|
+
URL url = new URL(
|
|
83
|
+
base + "/manifests/" + appId + "/" + channelKey + "/" + runtimeKey + "/manifest.json"
|
|
84
|
+
);
|
|
78
85
|
|
|
79
86
|
requireHTTPS(url, allowInsecureUrls);
|
|
80
87
|
|
|
81
88
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
82
89
|
try {
|
|
83
90
|
connection.setRequestMethod("GET");
|
|
84
|
-
connection.setRequestProperty("X-App-Id", appId);
|
|
85
|
-
connection.setRequestProperty("X-Platform", platform);
|
|
86
|
-
if (channel != null && !channel.trim().isEmpty()) {
|
|
87
|
-
connection.setRequestProperty("X-Channel", channel);
|
|
88
|
-
}
|
|
89
|
-
connection.setRequestProperty("X-Current-Version", currentVersion);
|
|
90
|
-
if (currentReleaseId != null && !currentReleaseId.trim().isEmpty()) {
|
|
91
|
-
connection.setRequestProperty("X-Release-Id", currentReleaseId);
|
|
92
|
-
}
|
|
93
|
-
if (runtimeVersion != null && !runtimeVersion.trim().isEmpty()) {
|
|
94
|
-
connection.setRequestProperty("X-Runtime-Version", runtimeVersion);
|
|
95
|
-
}
|
|
96
91
|
connection.setConnectTimeout(15_000);
|
|
97
92
|
connection.setReadTimeout(30_000);
|
|
98
93
|
|
|
99
94
|
int status = connection.getResponseCode();
|
|
100
|
-
if (status == 204) {
|
|
95
|
+
if (status == 404 || status == 204) {
|
|
101
96
|
return null;
|
|
102
97
|
}
|
|
103
98
|
if (status != 200) {
|
|
@@ -125,14 +120,18 @@ final class ManifestClient {
|
|
|
125
120
|
}
|
|
126
121
|
|
|
127
122
|
ManifestSignature signature = parseSignature(json.optJSONObject("signature"));
|
|
128
|
-
ManifestSignature signatureV2 = parseSignature(json.optJSONObject("signatureV2"));
|
|
129
123
|
|
|
130
124
|
String releaseId = null;
|
|
131
125
|
if (json.has("releaseId") && !json.isNull("releaseId")) {
|
|
132
|
-
releaseId = json.getString("releaseId");
|
|
126
|
+
releaseId = json.getString("releaseId").trim();
|
|
127
|
+
}
|
|
128
|
+
if (releaseId != null && releaseId.isEmpty()) {
|
|
129
|
+
releaseId = null;
|
|
130
|
+
}
|
|
131
|
+
if (releaseId == null) {
|
|
132
|
+
throw new IllegalStateException("Manifest response missing required releaseId");
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
// Validate download URL scheme
|
|
136
135
|
requireHTTPS(new URL(downloadUrl), allowInsecureUrls);
|
|
137
136
|
|
|
138
137
|
if (manifestKeys == null || manifestKeys.isEmpty()) {
|
|
@@ -142,36 +141,23 @@ final class ManifestClient {
|
|
|
142
141
|
);
|
|
143
142
|
}
|
|
144
143
|
|
|
145
|
-
// Verify manifest signature if signing keys are configured
|
|
146
144
|
if (manifestKeys != null && !manifestKeys.isEmpty()) {
|
|
147
|
-
if (
|
|
148
|
-
ManifestVerifier.verify(
|
|
149
|
-
appId,
|
|
150
|
-
channel,
|
|
151
|
-
platform,
|
|
152
|
-
version,
|
|
153
|
-
sha256,
|
|
154
|
-
size,
|
|
155
|
-
responseRuntimeVersion,
|
|
156
|
-
signatureV2,
|
|
157
|
-
manifestKeys
|
|
158
|
-
);
|
|
159
|
-
} else if (signature != null) {
|
|
160
|
-
ManifestVerifier.verifyLegacy(
|
|
161
|
-
appId,
|
|
162
|
-
channel,
|
|
163
|
-
platform,
|
|
164
|
-
version,
|
|
165
|
-
sha256,
|
|
166
|
-
size,
|
|
167
|
-
signature,
|
|
168
|
-
manifestKeys
|
|
169
|
-
);
|
|
170
|
-
} else {
|
|
145
|
+
if (signature == null) {
|
|
171
146
|
throw new IllegalStateException(
|
|
172
147
|
"Manifest signature missing but signing keys are configured"
|
|
173
148
|
);
|
|
174
149
|
}
|
|
150
|
+
|
|
151
|
+
ManifestVerifier.verify(
|
|
152
|
+
appId,
|
|
153
|
+
channel,
|
|
154
|
+
version,
|
|
155
|
+
sha256,
|
|
156
|
+
size,
|
|
157
|
+
responseRuntimeVersion,
|
|
158
|
+
signature,
|
|
159
|
+
manifestKeys
|
|
160
|
+
);
|
|
175
161
|
}
|
|
176
162
|
|
|
177
163
|
return new LatestManifest(
|
|
@@ -30,7 +30,6 @@ final class ManifestVerifier {
|
|
|
30
30
|
static void verify(
|
|
31
31
|
String appId,
|
|
32
32
|
String channel,
|
|
33
|
-
String platform,
|
|
34
33
|
String version,
|
|
35
34
|
String sha256,
|
|
36
35
|
int size,
|
|
@@ -41,7 +40,6 @@ final class ManifestVerifier {
|
|
|
41
40
|
String payload = buildCanonicalPayload(
|
|
42
41
|
appId,
|
|
43
42
|
channel,
|
|
44
|
-
platform,
|
|
45
43
|
version,
|
|
46
44
|
sha256,
|
|
47
45
|
size,
|
|
@@ -53,30 +51,6 @@ final class ManifestVerifier {
|
|
|
53
51
|
verifyPayload(payload, signature, trustedKeys);
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
static void verifyLegacy(
|
|
57
|
-
String appId,
|
|
58
|
-
String channel,
|
|
59
|
-
String platform,
|
|
60
|
-
String version,
|
|
61
|
-
String sha256,
|
|
62
|
-
int size,
|
|
63
|
-
ManifestClient.ManifestSignature signature,
|
|
64
|
-
List<KeyEntry> trustedKeys
|
|
65
|
-
) throws Exception {
|
|
66
|
-
String payload = buildLegacyCanonicalPayload(
|
|
67
|
-
appId,
|
|
68
|
-
channel,
|
|
69
|
-
platform,
|
|
70
|
-
version,
|
|
71
|
-
sha256,
|
|
72
|
-
size,
|
|
73
|
-
signature.kid,
|
|
74
|
-
signature.iat,
|
|
75
|
-
signature.exp
|
|
76
|
-
);
|
|
77
|
-
verifyPayload(payload, signature, trustedKeys);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
54
|
private static void verifyPayload(
|
|
81
55
|
String payload,
|
|
82
56
|
ManifestClient.ManifestSignature signature,
|
|
@@ -120,7 +94,6 @@ final class ManifestVerifier {
|
|
|
120
94
|
private static String buildCanonicalPayload(
|
|
121
95
|
String appId,
|
|
122
96
|
String channel,
|
|
123
|
-
String platform,
|
|
124
97
|
String version,
|
|
125
98
|
String sha256,
|
|
126
99
|
int size,
|
|
@@ -130,16 +103,13 @@ final class ManifestVerifier {
|
|
|
130
103
|
int exp
|
|
131
104
|
) {
|
|
132
105
|
return (
|
|
133
|
-
"
|
|
106
|
+
"MANIFEST\n" +
|
|
134
107
|
"appId:" +
|
|
135
108
|
appId +
|
|
136
109
|
"\n" +
|
|
137
110
|
"channel:" +
|
|
138
111
|
(channel != null ? channel : "null") +
|
|
139
112
|
"\n" +
|
|
140
|
-
"platform:" +
|
|
141
|
-
platform +
|
|
142
|
-
"\n" +
|
|
143
113
|
"version:" +
|
|
144
114
|
version +
|
|
145
115
|
"\n" +
|
|
@@ -163,50 +133,6 @@ final class ManifestVerifier {
|
|
|
163
133
|
);
|
|
164
134
|
}
|
|
165
135
|
|
|
166
|
-
private static String buildLegacyCanonicalPayload(
|
|
167
|
-
String appId,
|
|
168
|
-
String channel,
|
|
169
|
-
String platform,
|
|
170
|
-
String version,
|
|
171
|
-
String sha256,
|
|
172
|
-
int size,
|
|
173
|
-
String kid,
|
|
174
|
-
int iat,
|
|
175
|
-
int exp
|
|
176
|
-
) {
|
|
177
|
-
return (
|
|
178
|
-
"MANIFEST_V1\n" +
|
|
179
|
-
"appId:" +
|
|
180
|
-
appId +
|
|
181
|
-
"\n" +
|
|
182
|
-
"channel:" +
|
|
183
|
-
(channel != null ? channel : "null") +
|
|
184
|
-
"\n" +
|
|
185
|
-
"platform:" +
|
|
186
|
-
platform +
|
|
187
|
-
"\n" +
|
|
188
|
-
"version:" +
|
|
189
|
-
version +
|
|
190
|
-
"\n" +
|
|
191
|
-
"sha256:" +
|
|
192
|
-
sha256 +
|
|
193
|
-
"\n" +
|
|
194
|
-
"size:" +
|
|
195
|
-
size +
|
|
196
|
-
"\n" +
|
|
197
|
-
"minNativeBuild:null" +
|
|
198
|
-
"\n" +
|
|
199
|
-
"kid:" +
|
|
200
|
-
kid +
|
|
201
|
-
"\n" +
|
|
202
|
-
"iat:" +
|
|
203
|
-
iat +
|
|
204
|
-
"\n" +
|
|
205
|
-
"exp:" +
|
|
206
|
-
exp
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
136
|
private static byte[] base64UrlDecode(String input) {
|
|
211
137
|
// Convert base64url to standard base64
|
|
212
138
|
String base64 = input.replace('-', '+').replace('_', '/');
|