@ampless/backend 0.2.0-alpha.1 → 0.2.0-alpha.11
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.ja.md +130 -0
- package/README.md +3 -0
- package/dist/auth/post-confirmation.js +2 -0
- package/dist/auth/user-admin.d.ts +49 -0
- package/dist/auth/user-admin.js +107 -0
- package/dist/chunk-BYXBJQAS.js +0 -0
- package/dist/events/dispatcher.js +2 -0
- package/dist/events/processor-trusted.d.ts +6 -2
- package/dist/events/processor-trusted.js +2 -0
- package/dist/events/processor-untrusted.d.ts +6 -2
- package/dist/events/processor-untrusted.js +2 -0
- package/dist/functions/api-key-renewer.js +2 -0
- package/dist/functions/mcp-handler.d.ts +41 -0
- package/dist/functions/mcp-handler.js +754 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +88 -8
- package/package.json +15 -2
package/README.ja.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
> English: [README.md](./README.md)
|
|
2
|
+
>
|
|
3
|
+
|
|
4
|
+
# @ampless/backend
|
|
5
|
+
|
|
6
|
+
[ampless](https://github.com/heavymoons/ampless) 向け Amplify Gen 2 バックエンドファクトリー。IAM / SQS / DynamoDB ストリームの配線、認証 / データ / ストレージの定義、およびすべてのイベント処理 Lambda を `defineAmplessBackend(...)` ひとつのファクトリーにまとめます。
|
|
7
|
+
|
|
8
|
+
> **プレリリース / アルファ版。** v1.0 まではマイナーバージョンでも破壊的変更が入る可能性があります。
|
|
9
|
+
|
|
10
|
+
テンプレートから切り出すことで、スキャフォールダーを再実行せずに `npm update @ampless/backend` でアップデートできます。バックエンドのバグ修正やインフラ改善はパッケージ経由で届きます。ユーザー側の `amplify/` ツリーは、ファクトリーを組み合わせる 1〜5 行のシェルだけで済みます。
|
|
11
|
+
|
|
12
|
+
## インストール
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @ampless/backend@alpha ampless@alpha
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
ピア依存: `@aws-amplify/backend`(^1)、`aws-cdk-lib`(^2)。CLI スキャフォールダーがテンプレートの `package.json` に互換バージョンをピン留めします。
|
|
19
|
+
|
|
20
|
+
## 使い方
|
|
21
|
+
|
|
22
|
+
### `amplify/backend.ts`
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { defineAmplessBackend } from '@ampless/backend'
|
|
26
|
+
import { auth } from './auth/resource'
|
|
27
|
+
import { data } from './data/resource'
|
|
28
|
+
import { storage } from './storage/resource'
|
|
29
|
+
import { postConfirmation } from './auth/post-confirmation/resource'
|
|
30
|
+
import { eventDispatcher } from './events/dispatcher/resource'
|
|
31
|
+
import { processorTrusted } from './events/processor-trusted/resource'
|
|
32
|
+
import { processorUntrusted } from './events/processor-untrusted/resource'
|
|
33
|
+
import { apiKeyRenewer } from './functions/api-key-renewer/resource'
|
|
34
|
+
|
|
35
|
+
export default defineAmplessBackend({
|
|
36
|
+
auth, data, storage, postConfirmation,
|
|
37
|
+
eventDispatcher, processorTrusted, processorUntrusted, apiKeyRenewer,
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `amplify/auth/resource.ts`
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { defineAuth } from '@aws-amplify/backend'
|
|
45
|
+
import { amplessAuthConfig } from '@ampless/backend'
|
|
46
|
+
import { postConfirmation } from './post-confirmation/resource'
|
|
47
|
+
|
|
48
|
+
export const auth = defineAuth(amplessAuthConfig({ postConfirmation }))
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> `defineAuth` は `amplify/auth/resource.ts` 自体に記述する必要があります。Amplify Gen 2 のインポートパス検証機能が `defineAuth` / `defineData` / `defineStorage` の呼び出し元を検査し、他のファイル(`node_modules/@ampless/backend/...` 配下のラッパーを含む)から呼び出された場合に `Amplify Auth must be defined in amplify/auth/resource.ts` というエラーを投げます。`amplessAuthConfig` は props オブジェクトを返すため、ampless のデフォルト設定を失わずにこのファイルで `defineAuth(...)` を呼び出せます。
|
|
52
|
+
|
|
53
|
+
### `amplify/data/resource.ts`
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { a, defineData, type ClientSchema } from '@aws-amplify/backend'
|
|
57
|
+
import { amplessSchemaModels, defaultAuthorizationModes } from '@ampless/backend'
|
|
58
|
+
|
|
59
|
+
const schema = a.schema({
|
|
60
|
+
...amplessSchemaModels(a),
|
|
61
|
+
// カスタムモデルをここに追加 — 組み込みモデルと同居します:
|
|
62
|
+
// MyCustomModel: a.model({ ... }).authorization((allow) => [...]),
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export type Schema = ClientSchema<typeof schema>
|
|
66
|
+
export const data = defineData({ schema, authorizationModes: defaultAuthorizationModes })
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3 つの AppSync JS リゾルバーファイル(`list-published-posts.js`、`get-published-post.js`、`list-posts-by-tag.js`)はテンプレート内に残ります — AppSync がリゾルバーの `entry` パスを CDK synth 時に `defineData` を呼び出すファイルからの相対パスで解決するため、pnpm でシンボリックリンクされた `node_modules` パスは解決を通過できません。`amplify/data/` 以外の場所に移動する場合は、`amplessSchemaModels(a, { resolverPaths })` で新しいパスを渡してください。
|
|
70
|
+
|
|
71
|
+
### `amplify/storage/resource.ts`
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { defineStorage } from '@aws-amplify/backend'
|
|
75
|
+
import { amplessStorageConfig } from '@ampless/backend'
|
|
76
|
+
export const storage = defineStorage(amplessStorageConfig())
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
> 認証と同じインポートパス制約があります — `defineStorage` はこのファイルから直接呼び出す必要があります。`amplessStorageConfig` は props オブジェクトを返します。
|
|
80
|
+
|
|
81
|
+
### Lambda 薄型シェル
|
|
82
|
+
|
|
83
|
+
`amplify/auth/`、`amplify/events/`、`amplify/functions/` 内の各ハンドラーファイルは 1〜3 行の re-export になります。Amplify の esbuild がこのパッケージへのインポートを追跡し、実際のハンドラーコードを Lambda アーティファクトにバンドルします。
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// amplify/auth/post-confirmation/handler.ts
|
|
87
|
+
export { handler } from '@ampless/backend/auth/post-confirmation'
|
|
88
|
+
|
|
89
|
+
// amplify/events/dispatcher/handler.ts
|
|
90
|
+
export { handler } from '@ampless/backend/events/dispatcher'
|
|
91
|
+
|
|
92
|
+
// amplify/events/processor-trusted/handler.ts
|
|
93
|
+
import config from '../../../cms.config'
|
|
94
|
+
import { createProcessorTrustedHandler } from '@ampless/backend/events/processor-trusted'
|
|
95
|
+
export const handler = createProcessorTrustedHandler({
|
|
96
|
+
plugins: config.plugins,
|
|
97
|
+
site: config.site,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// amplify/events/processor-untrusted/handler.ts
|
|
101
|
+
import config from '../../../cms.config'
|
|
102
|
+
import { createProcessorUntrustedHandler } from '@ampless/backend/events/processor-untrusted'
|
|
103
|
+
export const handler = createProcessorUntrustedHandler({
|
|
104
|
+
plugins: config.plugins,
|
|
105
|
+
site: config.site,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// amplify/functions/api-key-renewer/handler.ts
|
|
109
|
+
export { handler } from '@ampless/backend/functions/api-key-renewer'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## サブパス
|
|
113
|
+
|
|
114
|
+
- `@ampless/backend` — `defineAmplessBackend`、`amplessAuthConfig`、`amplessStorageConfig`、`amplessSchemaModels`、`extendAmplessSchema`、`defaultAuthorizationModes`
|
|
115
|
+
- `@ampless/backend/auth/post-confirmation` — Lambda ハンドラー
|
|
116
|
+
- `@ampless/backend/events/dispatcher` — Lambda ハンドラー
|
|
117
|
+
- `@ampless/backend/events/processor-trusted` — `createProcessorTrustedHandler({ plugins, site })`
|
|
118
|
+
- `@ampless/backend/events/processor-untrusted` — `createProcessorUntrustedHandler({ plugins, site })`
|
|
119
|
+
- `@ampless/backend/functions/api-key-renewer` — Lambda ハンドラー
|
|
120
|
+
|
|
121
|
+
## テンプレートに残るもの
|
|
122
|
+
|
|
123
|
+
- `amplify/data/*.js` — AppSync JS リゾルバー(ファイルパス制約のため)。
|
|
124
|
+
- すべての `resource.ts` と `handler.ts` — 薄型シェルですが、Amplify の CDK synth がユーザー側で解決する `entry: './handler.ts'` パスを保持しています。
|
|
125
|
+
- `cms.config.ts` と `themes-registry.ts` — ユーザーが所有するカスタマイズサーフェス。
|
|
126
|
+
- `templates/<theme>/` 以下のテーマコンポーネント — ユーザーが所有。
|
|
127
|
+
|
|
128
|
+
## ライセンス
|
|
129
|
+
|
|
130
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
> 日本語版: [README.ja.md](./README.ja.md)
|
|
2
|
+
>
|
|
3
|
+
|
|
1
4
|
# @ampless/backend
|
|
2
5
|
|
|
3
6
|
Amplify Gen 2 backend factories for [ampless](https://github.com/heavymoons/ampless). Bundles the IAM / SQS / DynamoDB-stream wiring, the auth / data / storage definitions, and every event-processing Lambda behind one `defineAmplessBackend(...)` factory.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Handler } from 'aws-lambda';
|
|
2
|
+
|
|
3
|
+
type AdminRole = 'admin' | 'editor' | 'none';
|
|
4
|
+
interface AdminUserDto {
|
|
5
|
+
userId: string;
|
|
6
|
+
email: string;
|
|
7
|
+
role: AdminRole;
|
|
8
|
+
}
|
|
9
|
+
interface SetArgs {
|
|
10
|
+
userId: string;
|
|
11
|
+
role: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Event shape that Amplify Gen 2 actually delivers to a Lambda data
|
|
15
|
+
* source attached via `a.handler.function()`. The CDK generated
|
|
16
|
+
* AppSync resource is a PIPELINE resolver whose Lambda-invocation
|
|
17
|
+
* function uses a VTL request mapping template that emits a flat
|
|
18
|
+
* payload:
|
|
19
|
+
*
|
|
20
|
+
* {
|
|
21
|
+
* "operation": "Invoke",
|
|
22
|
+
* "payload": {
|
|
23
|
+
* "typeName": "Query",
|
|
24
|
+
* "fieldName": "listAdminUsers",
|
|
25
|
+
* "arguments": { ... },
|
|
26
|
+
* "identity": { ... },
|
|
27
|
+
* "source": ..., "request": ..., "prev": ...
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* Notably this is NOT the canonical `AppSyncResolverEvent` shape
|
|
32
|
+
* (which has `event.info.fieldName`). The `aws-lambda` package's
|
|
33
|
+
* `AppSyncResolverHandler` type is misleading here — typing the
|
|
34
|
+
* handler with it sends `event.info.fieldName` to `undefined` and
|
|
35
|
+
* blows up at runtime with
|
|
36
|
+
* `Cannot read properties of undefined (reading 'fieldName')`.
|
|
37
|
+
*/
|
|
38
|
+
interface UserAdminEvent {
|
|
39
|
+
typeName: string;
|
|
40
|
+
fieldName: string;
|
|
41
|
+
arguments: Partial<SetArgs>;
|
|
42
|
+
identity?: unknown;
|
|
43
|
+
source?: unknown;
|
|
44
|
+
request?: unknown;
|
|
45
|
+
prev?: unknown;
|
|
46
|
+
}
|
|
47
|
+
declare const handler: Handler<UserAdminEvent, AdminUserDto | AdminUserDto[] | null>;
|
|
48
|
+
|
|
49
|
+
export { handler };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import "../chunk-BYXBJQAS.js";
|
|
2
|
+
|
|
3
|
+
// src/auth/user-admin.ts
|
|
4
|
+
import {
|
|
5
|
+
CognitoIdentityProviderClient,
|
|
6
|
+
ListUsersCommand,
|
|
7
|
+
AdminListGroupsForUserCommand,
|
|
8
|
+
AdminAddUserToGroupCommand,
|
|
9
|
+
AdminRemoveUserFromGroupCommand
|
|
10
|
+
} from "@aws-sdk/client-cognito-identity-provider";
|
|
11
|
+
var cognito = new CognitoIdentityProviderClient({});
|
|
12
|
+
var ADMIN_GROUP = "ampless-admin";
|
|
13
|
+
var EDITOR_GROUP = "ampless-editor";
|
|
14
|
+
function requireUserPoolId() {
|
|
15
|
+
const v = process.env.AMPLESS_USER_POOL_ID;
|
|
16
|
+
if (!v) {
|
|
17
|
+
console.error("[user-admin] missing required env var AMPLESS_USER_POOL_ID");
|
|
18
|
+
throw new Error("user-admin: missing required env var AMPLESS_USER_POOL_ID");
|
|
19
|
+
}
|
|
20
|
+
return v;
|
|
21
|
+
}
|
|
22
|
+
function emailOf(user) {
|
|
23
|
+
const attr = user.Attributes?.find((a) => a.Name === "email");
|
|
24
|
+
return attr?.Value ?? "";
|
|
25
|
+
}
|
|
26
|
+
async function roleOf(userPoolId, username) {
|
|
27
|
+
const { Groups } = await cognito.send(
|
|
28
|
+
new AdminListGroupsForUserCommand({ UserPoolId: userPoolId, Username: username })
|
|
29
|
+
);
|
|
30
|
+
const names = (Groups ?? []).map((g) => g.GroupName);
|
|
31
|
+
if (names.includes(ADMIN_GROUP)) return "admin";
|
|
32
|
+
if (names.includes(EDITOR_GROUP)) return "editor";
|
|
33
|
+
return "none";
|
|
34
|
+
}
|
|
35
|
+
async function listAdminUsers(userPoolId) {
|
|
36
|
+
const { Users } = await cognito.send(
|
|
37
|
+
new ListUsersCommand({ UserPoolId: userPoolId, Limit: 60 })
|
|
38
|
+
);
|
|
39
|
+
const users = Users ?? [];
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const u of users) {
|
|
42
|
+
if (!u.Username) continue;
|
|
43
|
+
const role = await roleOf(userPoolId, u.Username);
|
|
44
|
+
out.push({ userId: u.Username, email: emailOf(u), role });
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
async function setAdminUserRole(userPoolId, userId, role) {
|
|
49
|
+
for (const group of [ADMIN_GROUP, EDITOR_GROUP]) {
|
|
50
|
+
await cognito.send(
|
|
51
|
+
new AdminRemoveUserFromGroupCommand({
|
|
52
|
+
UserPoolId: userPoolId,
|
|
53
|
+
Username: userId,
|
|
54
|
+
GroupName: group
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (role !== "none") {
|
|
59
|
+
await cognito.send(
|
|
60
|
+
new AdminAddUserToGroupCommand({
|
|
61
|
+
UserPoolId: userPoolId,
|
|
62
|
+
Username: userId,
|
|
63
|
+
GroupName: role === "admin" ? ADMIN_GROUP : EDITOR_GROUP
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const { Users } = await cognito.send(
|
|
68
|
+
new ListUsersCommand({
|
|
69
|
+
UserPoolId: userPoolId,
|
|
70
|
+
Filter: `sub = "${userId}"`,
|
|
71
|
+
Limit: 1
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
const user = Users?.[0];
|
|
75
|
+
const email = user ? emailOf(user) : "";
|
|
76
|
+
const finalRole = await roleOf(userPoolId, userId);
|
|
77
|
+
return { userId, email, role: finalRole };
|
|
78
|
+
}
|
|
79
|
+
var handler = async (event) => {
|
|
80
|
+
const userPoolId = requireUserPoolId();
|
|
81
|
+
const field = event.fieldName;
|
|
82
|
+
try {
|
|
83
|
+
if (field === "listAdminUsers") {
|
|
84
|
+
return await listAdminUsers(userPoolId);
|
|
85
|
+
}
|
|
86
|
+
if (field === "setAdminUserRole") {
|
|
87
|
+
const { userId, role } = event.arguments;
|
|
88
|
+
if (role !== "admin" && role !== "editor" && role !== "none") {
|
|
89
|
+
console.error(`[user-admin] invalid role: ${role}`);
|
|
90
|
+
throw new Error(`Invalid role: ${role}`);
|
|
91
|
+
}
|
|
92
|
+
if (!userId) {
|
|
93
|
+
console.error("[user-admin] missing userId argument");
|
|
94
|
+
throw new Error("Missing userId argument");
|
|
95
|
+
}
|
|
96
|
+
return await setAdminUserRole(userPoolId, userId, role);
|
|
97
|
+
}
|
|
98
|
+
console.error(`[user-admin] unsupported fieldName: ${field}`);
|
|
99
|
+
throw new Error(`Unsupported fieldName: ${field}`);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error(`[user-admin] ${field} failed:`, err);
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
export {
|
|
106
|
+
handler
|
|
107
|
+
};
|
|
File without changes
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SQSHandler } from 'aws-lambda';
|
|
2
|
-
import {
|
|
2
|
+
import { Config } from 'ampless';
|
|
3
3
|
|
|
4
4
|
interface CreateProcessorTrustedHandlerOpts {
|
|
5
5
|
/**
|
|
@@ -7,8 +7,12 @@ interface CreateProcessorTrustedHandlerOpts {
|
|
|
7
7
|
* trusted plugins itself so callers don't need to remember the
|
|
8
8
|
* filter, and so adding `privileged` later only touches the handler
|
|
9
9
|
* code in this package.
|
|
10
|
+
*
|
|
11
|
+
* Accepts the raw `Config['plugins']` shape (which permits string
|
|
12
|
+
* entries for future dynamic loading) — the runtime filter discards
|
|
13
|
+
* anything that isn't a plugin object.
|
|
10
14
|
*/
|
|
11
|
-
plugins?:
|
|
15
|
+
plugins?: Config['plugins'];
|
|
12
16
|
/**
|
|
13
17
|
* The `cms.config.site` block, surfaced to plugin hooks via
|
|
14
18
|
* `ctx.site`. Pass through from the thin shell — handlers must
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { SQSHandler } from 'aws-lambda';
|
|
2
|
-
import {
|
|
2
|
+
import { Config } from 'ampless';
|
|
3
3
|
|
|
4
4
|
interface CreateProcessorUntrustedHandlerOpts {
|
|
5
5
|
/**
|
|
6
6
|
* The full `cms.config.plugins` array. The handler filters down to
|
|
7
7
|
* untrusted plugins itself.
|
|
8
|
+
*
|
|
9
|
+
* Accepts the raw `Config['plugins']` shape (which permits string
|
|
10
|
+
* entries for future dynamic loading) — the runtime filter discards
|
|
11
|
+
* anything that isn't a plugin object.
|
|
8
12
|
*/
|
|
9
|
-
plugins?:
|
|
13
|
+
plugins?: Config['plugins'];
|
|
10
14
|
/**
|
|
11
15
|
* The `cms.config.site` block, surfaced to plugin hooks via
|
|
12
16
|
* `ctx.site` (read-only — untrusted plugins have no AWS-touching
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP HTTP endpoint Lambda. Phase 4: Bearer validation + JSON-RPC 2.0
|
|
3
|
+
* tool dispatch over AppSync IAM auth.
|
|
4
|
+
*
|
|
5
|
+
* 1. Reads KvStore directly (PK = 'mcp-tokens', SK = SHA-256 hex)
|
|
6
|
+
* to validate `Authorization: Bearer amk_...`. Same narrow IAM
|
|
7
|
+
* grant as Phase 3 (`dynamodb:GetItem` on the KvStore table).
|
|
8
|
+
* 2. Parses the incoming JSON-RPC envelope by hand (no MCP SDK
|
|
9
|
+
* stdio transport in a Lambda runtime — overkill for the three
|
|
10
|
+
* verbs we actually need).
|
|
11
|
+
* 3. Dispatches `tools/call` through the shared `@ampless/mcp-server/tools`
|
|
12
|
+
* registry. The GraphqlClient implementation hits AppSync with
|
|
13
|
+
* SigV4 (`AWS_IAM` auth mode), gated by `allow.resource(mcpHandler)
|
|
14
|
+
* .to(['query', 'mutate'])` on Post / PostTag in the schema.
|
|
15
|
+
*
|
|
16
|
+
* Function URL event format: Lambda Function URLs emit API Gateway
|
|
17
|
+
* HTTP v2 events (https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads).
|
|
18
|
+
* Headers arrive lowercased.
|
|
19
|
+
*
|
|
20
|
+
* Note: `upload_media` is filtered out — the StorageClient flow needs
|
|
21
|
+
* presigned S3 PUT URLs (the Lambda doesn't accept the binary body
|
|
22
|
+
* itself), which lands in Phase 5.
|
|
23
|
+
*/
|
|
24
|
+
interface FunctionUrlEvent {
|
|
25
|
+
headers?: Record<string, string | undefined>;
|
|
26
|
+
body?: string;
|
|
27
|
+
isBase64Encoded?: boolean;
|
|
28
|
+
requestContext?: {
|
|
29
|
+
http?: {
|
|
30
|
+
method?: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
interface FunctionUrlResult {
|
|
35
|
+
statusCode: number;
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
body: string;
|
|
38
|
+
}
|
|
39
|
+
declare const handler: (event: FunctionUrlEvent) => Promise<FunctionUrlResult>;
|
|
40
|
+
|
|
41
|
+
export { handler };
|