@everystack/cli 0.2.7 → 0.2.9
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/package.json +3 -7
- package/src/cli/aws.ts +22 -3
- package/src/handler/assets.ts +5 -0
- package/src/handler/index.ts +1 -0
- package/src/handler/manifest.ts +49 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "CLI and OTA updates for Expo apps on everystack",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"publishConfig": {
|
|
@@ -41,8 +41,9 @@
|
|
|
41
41
|
"lint": "tsc --noEmit"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"@aws-sdk/client-cloudfront": "3.1053.0",
|
|
44
45
|
"@aws-sdk/client-cloudfront-keyvaluestore": "3.1053.0",
|
|
45
|
-
"@aws-sdk/signature-v4a": "3.
|
|
46
|
+
"@aws-sdk/signature-v4a": "3.1048.0",
|
|
46
47
|
"glob": "13.0.6",
|
|
47
48
|
"node-forge": "1.4.0",
|
|
48
49
|
"structured-headers": "1.0.1",
|
|
@@ -50,7 +51,6 @@
|
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
53
|
"@everystack/server": ">=0.1.0",
|
|
53
|
-
"@aws-sdk/client-cloudfront": "3.1053.0",
|
|
54
54
|
"@aws-sdk/client-cloudwatch-logs": "3.1053.0",
|
|
55
55
|
"@aws-sdk/client-lambda": "3.1053.0",
|
|
56
56
|
"@aws-sdk/client-s3": "3.1053.0",
|
|
@@ -64,9 +64,6 @@
|
|
|
64
64
|
"@everystack/server": {
|
|
65
65
|
"optional": true
|
|
66
66
|
},
|
|
67
|
-
"@aws-sdk/client-cloudfront": {
|
|
68
|
-
"optional": true
|
|
69
|
-
},
|
|
70
67
|
"@aws-sdk/client-cloudwatch-logs": {
|
|
71
68
|
"optional": true
|
|
72
69
|
},
|
|
@@ -90,7 +87,6 @@
|
|
|
90
87
|
}
|
|
91
88
|
},
|
|
92
89
|
"devDependencies": {
|
|
93
|
-
"@aws-sdk/client-cloudfront": "3.1053.0",
|
|
94
90
|
"@aws-sdk/client-cloudwatch-logs": "3.1053.0",
|
|
95
91
|
"@aws-sdk/client-lambda": "3.1053.0",
|
|
96
92
|
"@aws-sdk/client-s3": "3.1053.0",
|
package/src/cli/aws.ts
CHANGED
|
@@ -136,6 +136,16 @@ export async function createInvalidation(
|
|
|
136
136
|
/**
|
|
137
137
|
* Write a versioning key to CloudFront KeyValueStore.
|
|
138
138
|
* KVS requires ETag for optimistic concurrency — DescribeKeyValueStore first.
|
|
139
|
+
*
|
|
140
|
+
* KVS endpoints require SigV4a (asymmetric) signing. The SDK discovers the
|
|
141
|
+
* SigV4a implementation via a mutable singleton in @smithy/signature-v4 that
|
|
142
|
+
* @smithy/signature-v4a populates as a side-effect import. When the dependency
|
|
143
|
+
* tree resolves duplicate copies of @smithy/signature-v4, the singleton splits:
|
|
144
|
+
* the signer registers into one copy while the KVS client reads from another.
|
|
145
|
+
*
|
|
146
|
+
* We bypass this entirely by constructing SignatureV4a explicitly and passing it
|
|
147
|
+
* as `signer` to the client. When `config.signer` is set, resolveAwsSdkSigV4Config
|
|
148
|
+
* uses it directly — SignatureV4MultiRegion and the container are never consulted.
|
|
139
149
|
*/
|
|
140
150
|
export async function putKvsKey(
|
|
141
151
|
region: string,
|
|
@@ -143,15 +153,24 @@ export async function putKvsKey(
|
|
|
143
153
|
key: string,
|
|
144
154
|
value: string,
|
|
145
155
|
): Promise<void> {
|
|
146
|
-
|
|
147
|
-
await import('@aws-sdk/
|
|
156
|
+
const { SignatureV4a } = await import('@aws-sdk/signature-v4a');
|
|
157
|
+
const { defaultProvider } = await import('@aws-sdk/credential-provider-node');
|
|
158
|
+
const { Hash } = await import('@smithy/core/serde');
|
|
148
159
|
const {
|
|
149
160
|
CloudFrontKeyValueStoreClient,
|
|
150
161
|
DescribeKeyValueStoreCommand,
|
|
151
162
|
PutKeyCommand,
|
|
152
163
|
} = await import('@aws-sdk/client-cloudfront-keyvaluestore');
|
|
153
164
|
|
|
154
|
-
if (!kvsClient)
|
|
165
|
+
if (!kvsClient) {
|
|
166
|
+
const signer = new SignatureV4a({
|
|
167
|
+
credentials: defaultProvider(),
|
|
168
|
+
region: '*',
|
|
169
|
+
service: 'cloudfront-keyvaluestore',
|
|
170
|
+
sha256: Hash.bind(null, 'sha256'),
|
|
171
|
+
});
|
|
172
|
+
kvsClient = new CloudFrontKeyValueStoreClient({ region, signer });
|
|
173
|
+
}
|
|
155
174
|
|
|
156
175
|
const desc = await kvsClient.send(
|
|
157
176
|
new DescribeKeyValueStoreCommand({ KvsARN: kvsArn })
|
package/src/handler/assets.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { UpdatesHandlerOptions } from './types';
|
|
2
2
|
|
|
3
|
+
const debug = process.env.DEBUG_UPDATES === '1';
|
|
4
|
+
|
|
3
5
|
export async function handleAssets(request: Request, options: UpdatesHandlerOptions): Promise<Response> {
|
|
4
6
|
const url = new URL(request.url);
|
|
5
7
|
const key = url.searchParams.get('key');
|
|
6
8
|
|
|
7
9
|
if (!key) {
|
|
10
|
+
console.warn('[updates] asset request rejected: no key');
|
|
8
11
|
return new Response(JSON.stringify({ error: 'No asset key provided.' }), {
|
|
9
12
|
status: 400,
|
|
10
13
|
headers: { 'content-type': 'application/json' },
|
|
@@ -15,6 +18,7 @@ export async function handleAssets(request: Request, options: UpdatesHandlerOpti
|
|
|
15
18
|
// Lambda function URLs cap responses at 6 MB; Hermes bytecode bundles exceed that.
|
|
16
19
|
if (options.storage.presignGet) {
|
|
17
20
|
const presignedUrl = await options.storage.presignGet(key);
|
|
21
|
+
debug && console.log('[updates] asset redirect', { key });
|
|
18
22
|
return new Response(null, {
|
|
19
23
|
status: 302,
|
|
20
24
|
headers: {
|
|
@@ -29,6 +33,7 @@ export async function handleAssets(request: Request, options: UpdatesHandlerOpti
|
|
|
29
33
|
const result = await options.storage.get(key);
|
|
30
34
|
|
|
31
35
|
if (!result) {
|
|
36
|
+
console.warn('[updates] asset not found', { key });
|
|
32
37
|
return new Response(JSON.stringify({ error: `Asset "${key}" not found.` }), {
|
|
33
38
|
status: 404,
|
|
34
39
|
headers: { 'content-type': 'application/json' },
|
package/src/handler/index.ts
CHANGED
|
@@ -71,6 +71,7 @@ export function createUpdatesHandler(options: UpdatesHandlerOptions): (request:
|
|
|
71
71
|
return handleEditChannel(request, options, channelName);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
console.warn('[updates] 404 unmatched route', { method: request.method, pathname });
|
|
74
75
|
return new Response(JSON.stringify({ error: 'Not found' }), {
|
|
75
76
|
status: 404,
|
|
76
77
|
headers: { 'content-type': 'application/json' },
|
package/src/handler/manifest.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { buildMultipartResponse } from './multipart';
|
|
|
6
6
|
import { signRSASHA256, convertSHA256HashToUUID, convertToDictionaryItemsRepresentation } from './signing';
|
|
7
7
|
import { jsonError } from './helpers';
|
|
8
8
|
|
|
9
|
+
const debug = process.env.DEBUG_UPDATES === '1';
|
|
10
|
+
|
|
9
11
|
export async function handleManifest(request: Request, options: UpdatesHandlerOptions): Promise<Response> {
|
|
10
12
|
const headers = request.headers;
|
|
11
13
|
|
|
@@ -14,24 +16,46 @@ export async function handleManifest(request: Request, options: UpdatesHandlerOp
|
|
|
14
16
|
|
|
15
17
|
const platform = headers.get('expo-platform');
|
|
16
18
|
if (platform !== 'ios' && platform !== 'android') {
|
|
19
|
+
console.warn('[updates] manifest request rejected: unsupported platform', {
|
|
20
|
+
platform: headers.get('expo-platform'),
|
|
21
|
+
});
|
|
17
22
|
return jsonError(400, 'Unsupported platform. Expected either ios or android.');
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
const runtimeVersion = headers.get('expo-runtime-version');
|
|
21
26
|
if (!runtimeVersion) {
|
|
27
|
+
console.warn('[updates] manifest request rejected: no runtimeVersion');
|
|
22
28
|
return jsonError(400, 'No runtimeVersion provided.');
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
const channelName = headers.get('expo-channel-name') || options.defaultChannel || 'production';
|
|
26
32
|
|
|
33
|
+
debug && console.log('[updates] manifest request', {
|
|
34
|
+
platform,
|
|
35
|
+
runtimeVersion,
|
|
36
|
+
channelName,
|
|
37
|
+
currentUpdateId: headers.get('expo-current-update-id'),
|
|
38
|
+
protocolVersion,
|
|
39
|
+
expectSignature: !!headers.get('expo-expect-signature'),
|
|
40
|
+
});
|
|
41
|
+
|
|
27
42
|
// Try S3 flat-file manifest first (works without DB)
|
|
43
|
+
const s3Key = `${channelName}/${platform}/${runtimeVersion}/manifest.json`;
|
|
28
44
|
const s3Manifest = await tryS3Manifest(options, channelName, platform, runtimeVersion);
|
|
29
45
|
if (s3Manifest) {
|
|
46
|
+
debug && console.log('[updates] S3 manifest found', {
|
|
47
|
+
key: s3Key,
|
|
48
|
+
updateId: s3Manifest.id,
|
|
49
|
+
assetCount: s3Manifest.assets?.length,
|
|
50
|
+
hasLaunchAsset: !!s3Manifest.launchAsset,
|
|
51
|
+
});
|
|
30
52
|
return buildManifestResponse(request, s3Manifest, options, protocolVersion);
|
|
31
53
|
}
|
|
54
|
+
debug && console.log('[updates] S3 manifest not found', { key: s3Key });
|
|
32
55
|
|
|
33
56
|
// Fall back to DB-based lookup (V2)
|
|
34
57
|
if (!options.db) {
|
|
58
|
+
debug && console.log('[updates] no DB configured, returning 404');
|
|
35
59
|
return jsonError(404, `No update found for ${channelName}/${platform}/${runtimeVersion}`);
|
|
36
60
|
}
|
|
37
61
|
|
|
@@ -50,13 +74,16 @@ export async function handleManifest(request: Request, options: UpdatesHandlerOp
|
|
|
50
74
|
.limit(1);
|
|
51
75
|
|
|
52
76
|
if (!channel) {
|
|
77
|
+
console.warn('[updates] DB channel not found', { channelName });
|
|
53
78
|
return jsonError(404, `Channel "${channelName}" not found.`);
|
|
54
79
|
}
|
|
55
80
|
|
|
56
81
|
const branchRef = (channel.data as Record<string, unknown>)?.branchRef as string;
|
|
57
82
|
if (!branchRef) {
|
|
83
|
+
console.warn('[updates] DB channel has no branch mapping', { channelName });
|
|
58
84
|
return jsonError(404, `Channel "${channelName}" has no branch mapping.`);
|
|
59
85
|
}
|
|
86
|
+
debug && console.log('[updates] DB channel resolved', { channelName, branchRef });
|
|
60
87
|
|
|
61
88
|
// Look up latest release (buildArtifact type='ota') via run branchRef
|
|
62
89
|
const releaseResults = await options.db
|
|
@@ -74,9 +101,15 @@ export async function handleManifest(request: Request, options: UpdatesHandlerOp
|
|
|
74
101
|
.limit(1);
|
|
75
102
|
|
|
76
103
|
if (!releaseResults[0]) {
|
|
104
|
+
console.warn('[updates] DB release not found', { branchRef, runtimeVersion, platform });
|
|
77
105
|
return jsonError(404, `No update found for runtime version: ${runtimeVersion}`);
|
|
78
106
|
}
|
|
79
107
|
release = releaseResults[0].release;
|
|
108
|
+
debug && console.log('[updates] DB release found', {
|
|
109
|
+
releaseId: release.id,
|
|
110
|
+
runId: release.runId,
|
|
111
|
+
updateId: (release.data as Record<string, unknown>)?.updateId,
|
|
112
|
+
});
|
|
80
113
|
|
|
81
114
|
// Look up assets (buildArtifacts type='ota_asset') for this release's run
|
|
82
115
|
assets = await options.db
|
|
@@ -86,8 +119,12 @@ export async function handleManifest(request: Request, options: UpdatesHandlerOp
|
|
|
86
119
|
eq(buildArtifacts.type, 'ota_asset'),
|
|
87
120
|
eq(buildArtifacts.runId, release.runId),
|
|
88
121
|
));
|
|
89
|
-
} catch {
|
|
122
|
+
} catch (err) {
|
|
90
123
|
// DB unavailable or tables don't exist — not an error in V1
|
|
124
|
+
console.error('[updates] DB lookup error', {
|
|
125
|
+
error: err instanceof Error ? err.message : String(err),
|
|
126
|
+
channelName, platform, runtimeVersion,
|
|
127
|
+
});
|
|
91
128
|
return jsonError(404, `No update found for ${channelName}/${platform}/${runtimeVersion}`);
|
|
92
129
|
}
|
|
93
130
|
|
|
@@ -95,6 +132,7 @@ export async function handleManifest(request: Request, options: UpdatesHandlerOp
|
|
|
95
132
|
|
|
96
133
|
// Handle rollback
|
|
97
134
|
if (releaseData.isRollback) {
|
|
135
|
+
debug && console.log('[updates] response: rollback', { protocolVersion });
|
|
98
136
|
if (protocolVersion === 0) {
|
|
99
137
|
return jsonError(400, 'Rollbacks not supported on protocol version 0');
|
|
100
138
|
}
|
|
@@ -106,6 +144,9 @@ export async function handleManifest(request: Request, options: UpdatesHandlerOp
|
|
|
106
144
|
const updateId = releaseData.updateId as string;
|
|
107
145
|
const releaseUUID = convertSHA256HashToUUID(updateId);
|
|
108
146
|
if (currentUpdateId === releaseUUID && protocolVersion === 1) {
|
|
147
|
+
debug && console.log('[updates] response: noUpdateAvailable', {
|
|
148
|
+
currentUpdateId, releaseUUID,
|
|
149
|
+
});
|
|
109
150
|
return buildNoUpdateAvailableResponse(request, options, protocolVersion);
|
|
110
151
|
}
|
|
111
152
|
|
|
@@ -148,6 +189,13 @@ export async function handleManifest(request: Request, options: UpdatesHandlerOp
|
|
|
148
189
|
},
|
|
149
190
|
};
|
|
150
191
|
|
|
192
|
+
debug && console.log('[updates] response: manifest', {
|
|
193
|
+
updateId: releaseUUID,
|
|
194
|
+
assetCount: regularAssets.length,
|
|
195
|
+
hasLaunchAsset: !!launchAsset,
|
|
196
|
+
source: 'db',
|
|
197
|
+
});
|
|
198
|
+
|
|
151
199
|
return buildManifestResponse(request, manifest, options, protocolVersion);
|
|
152
200
|
}
|
|
153
201
|
|