@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everystack/cli",
3
- "version": "0.2.7",
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.1031.0",
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
- // KVS requires SigV4a (asymmetric) signing — load pure-JS implementation
147
- await import('@aws-sdk/signature-v4a');
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) kvsClient = new CloudFrontKeyValueStoreClient({ region });
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 })
@@ -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' },
@@ -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' },
@@ -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