feature: upgrade to Deno 2

#1
This commit is contained in:
Alexander
2024-11-20 18:31:51 +09:00
committed by GitHub
4 changed files with 97 additions and 41 deletions

View File

@@ -7,7 +7,7 @@ name: Docker
on:
push:
branches: [ "main" ]
branches: [ "main", "dev" ]
tags: [ "*" ]
env:

View File

@@ -1,5 +1,5 @@
# Use the official Deno image
FROM denoland/deno:1.46.3
FROM denoland/deno:2.0.6
# Set working directory
WORKDIR /app

25
deno.lock generated
View File

@@ -1,14 +1,21 @@
{
"version": "3",
"packages": {
"specifiers": {
"npm:hono": "npm:hono@4.6.10"
"version": "4",
"specifiers": {
"jsr:@b-fuze/deno-dom@*": "0.1.48",
"jsr:@libs/xml@*": "6.0.1",
"npm:hono@*": "4.6.10"
},
"jsr": {
"@b-fuze/deno-dom@0.1.48": {
"integrity": "bf5b591aef2e9e9c59adfcbb93a9ecd45bab5b7c8263625beafa5c8f1662e7da"
},
"npm": {
"hono@4.6.10": {
"integrity": "sha512-IXXNfRAZEahFnWBhUUlqKEGF9upeE6hZoRZszvNkyAz/CYp+iVbxm3viMvStlagRJohjlBRGOQ7f4jfcV0XMGg==",
"dependencies": {}
}
"@libs/xml@6.0.1": {
"integrity": "64af4f93464c77c3e1158fb97c3657779ca554b14f38616b96cde31e22d8a309"
}
},
"npm": {
"hono@4.6.10": {
"integrity": "sha512-IXXNfRAZEahFnWBhUUlqKEGF9upeE6hZoRZszvNkyAz/CYp+iVbxm3viMvStlagRJohjlBRGOQ7f4jfcV0XMGg=="
}
},
"redirects": {

View File

@@ -2,6 +2,7 @@ import { ListObjectsOptions, ListObjectsResult, StorageObject } from "../types/m
import { S3, type ListObjectsV2Request } from "https://deno.land/x/aws_api@v0.8.1/services/s3/mod.ts";
import { ApiFactory } from "https://deno.land/x/aws_api@v0.8.1/client/mod.ts";
import { getSignedUrl } from "https://deno.land/x/aws_s3_presign@2.2.1/mod.ts";
import { parse as parseXml } from "jsr:@libs/xml";
interface Credentials {
awsAccessKeyId: string;
@@ -11,10 +12,14 @@ interface Credentials {
export class StorageService {
private client: S3;
private credentials: Credentials;
private credentials!: Credentials;
constructor(private bucket: string, private region: string) {
this.credentials = this.resolveCredentials();
this.initializeClient();
}
private async initializeClient() {
this.credentials = await this.resolveCredentials();
const factory = new ApiFactory({
region: this.region,
credentials: this.credentials,
@@ -23,28 +28,35 @@ export class StorageService {
this.client = new S3(factory);
}
private async fetchInstanceProfileCredentials(): Promise<Credentials | null> {
// Make sure any method that uses this.client waits for initialization
private async ensureInitialized() {
if (!this.client) {
await this.initializeClient();
}
}
private async fetchInstanceProfileCredentials(): Promise<[Credentials | null, string | null]> {
try {
const metadataUrl = "http://169.254.169.254/latest/meta-data/iam/security-credentials/";
const roleName = await (await fetch(metadataUrl)).text();
const credentials = await (await fetch(`${metadataUrl}${roleName}`)).json();
return {
return [{
awsAccessKeyId: credentials.AccessKeyId,
awsSecretKey: credentials.SecretAccessKey,
sessionToken: credentials.Token,
};
} catch {
return null;
}, null];
} catch (error) {
return [null, `Instance profile credentials failed: ${error instanceof Error ? error.message : String(error)}`];
}
}
private async fetchIRSACredentials(): Promise<Credentials | null> {
private async fetchIRSACredentials(): Promise<[Credentials | null, string | null]> {
const roleArn = Deno.env.get("AWS_ROLE_ARN");
const tokenFile = Deno.env.get("AWS_WEB_IDENTITY_TOKEN_FILE");
if (!roleArn || !tokenFile) {
return null;
return [null, "IRSA credentials not configured: missing AWS_ROLE_ARN or AWS_WEB_IDENTITY_TOKEN_FILE"];
}
try {
@@ -52,47 +64,53 @@ export class StorageService {
const sts = new WebIdentityCredentials(roleArn, token);
const creds = await sts.getCredentials();
return {
return [{
awsAccessKeyId: creds.accessKeyId,
awsSecretKey: creds.secretAccessKey,
sessionToken: creds.sessionToken,
};
} catch {
return null;
}, null];
} catch (error) {
return [null, `IRSA credentials failed: ${error instanceof Error ? error.message : String(error)}`];
}
}
private getStaticCredentials(): Credentials | null {
private getStaticCredentials(): [Credentials | null, string | null] {
const accessKeyId = Deno.env.get("AWS_ACCESS_KEY_ID");
const secretKey = Deno.env.get("AWS_SECRET_ACCESS_KEY");
if (!accessKeyId || !secretKey) {
return null;
return [null, "Static credentials not configured: missing AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY"];
}
return {
return [{
awsAccessKeyId: accessKeyId,
awsSecretKey: secretKey,
};
}, null];
}
private async resolveCredentials(): Promise<Credentials> {
const errors: string[] = [];
// Try IRSA first
const irsaCreds = await this.fetchIRSACredentials();
const [irsaCreds, irsaError] = await this.fetchIRSACredentials();
if (irsaCreds) return irsaCreds;
if (irsaError) errors.push(irsaError);
// Then try static credentials
const staticCreds = this.getStaticCredentials();
const [staticCreds, staticError] = this.getStaticCredentials();
if (staticCreds) return staticCreds;
if (staticError) errors.push(staticError);
// Finally try instance profile
const instanceCreds = await this.fetchInstanceProfileCredentials();
const [instanceCreds, instanceError] = await this.fetchInstanceProfileCredentials();
if (instanceCreds) return instanceCreds;
if (instanceError) errors.push(instanceError);
throw new Error("No valid AWS credentials found");
throw new Error(`No valid AWS credentials found:\n${errors.join('\n')}`);
}
async getSignedUrl(key: string, expiresIn = 3600): Promise<string> {
await this.ensureInitialized();
return await getSignedUrl({
accessKeyId: this.credentials.awsAccessKeyId,
secretAccessKey: this.credentials.awsSecretKey,
@@ -104,6 +122,7 @@ export class StorageService {
}
async listObjects(options: ListObjectsOptions): Promise<ListObjectsResult> {
await this.ensureInitialized();
try {
const params: ListObjectsV2Request = {
Bucket: this.bucket,
@@ -169,24 +188,54 @@ class WebIdentityCredentials {
WebIdentityToken: this.token,
});
const response = await fetch(`https://sts.amazonaws.com?${params}`, {
const url = `https://sts.amazonaws.com?${params}`;
const response = await fetch(url, {
method: "GET",
});
const responseText = await response.text();
if (!response.ok) {
throw new Error(`Failed to assume role: ${await response.text()}`);
console.error('IRSA Request Failed:', {
url,
requestHeaders: {
method: "GET",
RoleArn: this.roleArn,
TokenLength: this.token.length,
},
responseStatus: response.status,
responseHeaders: Object.fromEntries(response.headers),
responseBody: responseText
});
throw new Error(`Failed to assume role: ${responseText}`);
}
const xml = await response.text();
const result = new DOMParser().parseFromString(xml, "text/xml");
const result = parseXml(responseText);
const credentials = result.AssumeRoleWithWebIdentityResponse?.AssumeRoleWithWebIdentityResult?.Credentials;
const credentials = result.querySelector("Credentials");
if (!credentials) throw new Error("No credentials in response");
if (!credentials) {
console.error('IRSA Parsing Failed:', {
parsedXml: JSON.stringify(result, null, 2),
rawResponse: responseText
});
throw new Error("No credentials in response. Full response: " + JSON.stringify(result, null, 2));
}
const { AccessKeyId, SecretAccessKey, SessionToken } = credentials;
if (!AccessKeyId || !SecretAccessKey || !SessionToken) {
console.error('IRSA Missing Fields:', {
hasAccessKeyId: !!AccessKeyId,
hasSecretAccessKey: !!SecretAccessKey,
hasSessionToken: !!SessionToken,
parsedCredentials: credentials
});
throw new Error("Missing required credential fields in response");
}
return {
accessKeyId: credentials.querySelector("AccessKeyId")?.textContent ?? "",
secretAccessKey: credentials.querySelector("SecretAccessKey")?.textContent ?? "",
sessionToken: credentials.querySelector("SessionToken")?.textContent ?? "",
accessKeyId: AccessKeyId,
secretAccessKey: SecretAccessKey,
sessionToken: SessionToken,
};
}
}