Lambda（Node.js）でBox Platform APIを使用する

## APIを実行するBOXユーザアカウントについて
管理対象ユーザ（通常のBoxユーザ）とBox Platformユーザがあります。
管理対象ユーザを特定の個人と関連づけられないユースケースで使用することは禁止されています。APIを使用してバッチ処理を行う場合などは必ずBox Platformユーザを使用します。

- [Fair Use Policy | Box](https://www.box.com/legal/fairusepolicy)
- [ユーザータイプ - Box Developerドキュメント](https://ja.developer.box.com/guides/getting-started/user-types/)

## カスタムアプリの作成
Box Platform APIを使用するにはBoxでマイアプリを作成する必要があります。

1. Boxの開発者コンソールにアクセスする。
    - 初回は[こちら](https://ja.developer.box.com/)の「開発者コンソールに移動」からアクセスする。一度アクセスすると、Box UIの左下に開発者コンソールのリンクが表示される。
2. `マイアプリ > アプリ`の新規作成をクリックする。
3. カスタムアプリをクリックし、アプリ名と目的を入力する（後で変更可能）。認証方法は「サーバー認証（JWT使用）」を選択する。
4. 構成タブの設定
    - 「アプリアクセスレベル」を「アプリ＋Enterpriseアクセス」に変更する。
    - アプリケーションスコープで必要な権限にチェックをつける。
    - 管理対象ユーザとしてAPIを実行する場合は高度な機能の「as-userヘッダーを使用してAPI呼び出しを行う」にチェックをつける。
    - [Box UI Elements](https://ja.developer.box.com/guides/embed/ui-elements/)などで、管理対象ユーザのアクセストークンが必要な場合は、高度な機能の「ユーザーアクセストークンを生成する」にチェックをつける。
    - クライアントサイドからAPIを実行する場合はCORSドメインを登録する。
5. 構成タブの公開キーの追加と管理から、「公開/秘密キーペアを生成」をクリックし、config.jsonを作成しておく。
6. 承認タブ「確認して送信」をクリックし、管理コンソールの`アプリ > カスタムアプリマネージャ`でアプリケーションの承認を行う。 

## Lambda
[Box Node.js SDK](https://github.com/box/box-node-sdk)があるため、簡単に実装できます。

```console
% npm install --save box-node-sdk
```

まず上記の「カスタムアプリの作成」で作成したアプリの認証情報を使用してSDKを初期化します。次にBox APIクライアントを作成します。
下記はconfig.jsonを使った認証の例になります。[公式のドキュメント](https://github.com/box/box-node-sdk#creating-api-clients)には他の方法の認証方法も記載されています。
Box Node.js SDKがサポートしている機能は[こちら](https://github.com/box/box-node-sdk/blob/main/docs/README.md)。
シークレットマネージャーについては[こちら](https://qiita.com/TaaaZyyy/items/db7728afdb07d5851161)で記事にしています。

```typescript:src/lambda/sample/index.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import axios from 'axios';
import BoxSDKNode = require('box-node-sdk');

const AWS_SESSION_TOKEN = process.env['AWS_SESSION_TOKEN'] || '';
const PARAMETERS_SECRETS_EXTENSION_HTTP_PORT = process.env['PARAMETERS_SECRETS_EXTENSION_HTTP_PORT'] || '';

export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
    const boxConfig = await getBoxConfig();

    const sdk = BoxSDKNode.getPreconfiguredInstance(boxConfig);
    const client = sdk.getAppAuthClient('enterprise');
    // 管理対象ユーザまたはAPPユーザとしてAPIを実行する場合は下記
    // const client = sdk.getAppAuthClient('user', userId);

    // APIの実行
    const userInfo = await client.users.get(client.CURRENT_USER_ID);

    return {
        statusCode: 200,
        body: JSON.stringify(userInfo),
    };
}

async function getBoxConfig(): Promise<object> {
    const smUri = `http://localhost:${PARAMETERS_SECRETS_EXTENSION_HTTP_PORT}/secretsmanager/get`;
    const res = await axios.get(smUri, {
        params: { secretId: '/box/login' },
        headers: { 'X-Aws-Parameters-Secrets-Token': AWS_SESSION_TOKEN },
    });
    return JSON.parse(res.data.SecretString);
}

```

## AWS CDK
デプロイ後、シークレットマネージャーにBoxの開発者コンソールでダウンロードしたconfig.jsonの値を設定します。

```typescript:lib/sample-stack.ts
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaNodeJs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as sm from 'aws-cdk-lib/aws-secretsmanager';

export class SampleStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        // Secrets Manager
        const secret = this.createBoxSecret();

        // Lambda
        const lambda = this.createFunc();
        secret.grantRead(lambda);
    }

    /**
     * @description BOX認証情報を格納するシークレットを作成
     * {@link class Secret | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_secretsmanager.Secret.html}
     */
    private createBoxSecret(): sm.ISecret {
        return new sm.Secret(this, 'Secret', {
            secretName: '/box/login',
            description: 'boxからダウンロードしたconfig.jsonの内容を貼り付けてください。',
            secretStringValue: cdk.SecretValue.unsafePlainText('dummy'),
        });
    }

    /**
     * @description Lambdaの作成
     * {@link ParamsAndSecretsLayerVersion | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.ParamsAndSecretsLayerVersion.html}
     * {@link NodejsFunction | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html}
     */
    private createFunc(): lambdaNodeJs.NodejsFunction {
        // AWS Parameters and Secrets Lambda Extension の作成
        const paramsAndSecrets = lambda.ParamsAndSecretsLayerVersion.fromVersion(
            lambda.ParamsAndSecretsVersions.V1_0_103,
            {
                cacheSize: 500,
                logLevel: lambda.ParamsAndSecretsLogLevel.DEBUG,
            }
        );

        return new lambdaNodeJs.NodejsFunction(this, 'SampleFunc', {
            entry: 'src/lambda/sample/index.ts',
            handler: 'handler',
            runtime: lambda.Runtime.NODEJS_18_X,
            timeout: cdk.Duration.minutes(5),
            paramsAndSecrets,
        });
    }
}
```

## テスト
<details><summary>test/sample.test.ts</summary>

```typescript:test/sample.test.ts
import * as cdk from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { SampleStack } from '../lib/sample-stack';

test('SM', () => {
    const app = new cdk.App();
    const stack = new SampleStack(app, 'SampleStack');
    const template = Template.fromStack(stack);

    // Secrets Manager
    template.resourceCountIs('AWS::SecretsManager::Secret', 1);
    template.hasResourceProperties('AWS::SecretsManager::Secret', {
        Description: 'boxからダウンロードしたconfig.jsonの内容を貼り付けてください。',
        Name: '/box/login',
        SecretString: 'dummy',
    });
});

test('Lambda', () => {
    const app = new cdk.App();
    const stack = new SampleStack(app, 'SampleStack');
    const template = Template.fromStack(stack);

    // Lambda
    template.resourceCountIs('AWS::Lambda::Function', 1);
    template.hasResourceProperties('AWS::Lambda::Function', {
        Runtime: 'nodejs18.x',
        Timeout: 300,
    });
});
```

</details>

## エラー
### error TS1259: Module '"sample/node_modules/box-node-sdk/lib/managers/collaborations"' can only be default-imported using the 'esModuleInterop' flag
tsconfig.jsonでesModuleInteropをtrueにしないとトランスパイルに失敗します。tsconfig.jsonを下記のように修正することで解消します。

```diff_json:tsconfig.json
{
    "compilerOptions": {
        "target": "ES2020",
        "module": "commonjs",
        "lib": ["es2020", "dom"],
        "declaration": true,
        "strict": true,
        "noImplicitAny": true,
        "strictNullChecks": true,
        "noImplicitThis": true,
        "alwaysStrict": true,
        "noUnusedLocals": false,
        "noUnusedParameters": false,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": false,
        "inlineSourceMap": true,
        "inlineSources": true,
        "experimentalDecorators": true,
        "strictPropertyInitialization": false,
        "typeRoots": ["./node_modules/@types"],
+       "esModuleInterop": true
    },
    "exclude": ["node_modules", "cdk.out", "test"]
}
```

### Configuration does not include boxAppSettings object.
シークレットマネージャーに設定している値の形式が間違っています。下記のような形式である必要があります。

```json
{
  "boxAppSettings": {
    "clientID": "aaaaaaaa...",
    "clientSecret": "bbbbbbbb...",
    "appAuth": {
      "publicKeyID": "cccccccc",
      "privateKey": "-----BEGIN ENCRYPTED PRIVATE KEY-----\n...\n-----END ENCRYPTED PRIVATE KEY-----\n",
      "passphrase": "eeee..."
    }
  },
  "enterpriseID": "1234567890"
}
```

## 参考
- [JSDoc: Home](https://rawgit.com/box/box-node-sdk/main/docs/jsdoc/index.html)
- [box-node-sdk/docs/README.md at main · box/box-node-sdk](https://github.com/box/box-node-sdk/blob/main/docs/README.md)
- [APIリファレンス - Box Developerドキュメント](https://ja.developer.box.com/reference/)