@aws-amplify/core
Version:
Core category of aws-amplify
137 lines (128 loc) • 6.27 kB
text/typescript
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { AuthClass } from '../singleton/Auth';
import {
AmplifyOutputsUnknown,
LegacyConfig,
LibraryOptions,
ResourcesConfig,
} from '../singleton/types';
import { deepFreeze } from '../utils/deepFreeze';
import { parseAmplifyConfig } from '../utils/parseAmplifyConfig';
import { AmplifyContext } from './AmplifyContext';
import { AMPLIFY_CONTEXT_BRAND } from './contextBrand';
import { createAmplifyContextToken } from './contextToken';
/**
* Creates a local, branded {@link AmplifyContext} from the given resource
* configuration.
*
* Unlike `Amplify.configure()`, the returned context is **not** set as the
* global context and no Hub events are dispatched. Category APIs that receive
* this context (as their first positional argument) will use it instead of the
* global one, enabling per-request context injection in server-side /
* multi-tenant scenarios.
*
* The factory follows the "factory-provider" style: it instantiates a **fresh,
* per-context** {@link AuthClass} (never the process-wide singleton) whose
* `fetchAuthSession` / `clearCredentials` / `getTokens` closures back the
* returned context's methods. The context is branded via
* {@link AMPLIFY_CONTEXT_BRAND} and frozen so it can be recognized at runtime
* by `isAmplifyContext()` and cannot be mutated after creation.
*
* @remarks
* `@aws-amplify/core` sits at the bottom of the dependency graph and therefore
* cannot import the Cognito token/credentials provider factories (those live in
* the `aws-amplify` umbrella package and depend on `@aws-amplify/auth`). As a
* result this core-level factory does **not** inject default providers: callers
* that need auth must supply `libraryOptions.Auth.{tokenProvider,credentialsProvider}`.
* The `aws-amplify` package wraps this factory (Phase C2) to resolve and inject
* the default Cognito providers (SSR cookie storage vs. localStorage) before
* delegating here.
*
* @param resourceConfig - Back-end resource configuration (typed config,
* legacy `aws-exports`, or `amplify_outputs`).
* @param libraryOptions - Optional library options (e.g. Auth token/credentials
* providers, `ssr`).
* @param internalOptions - Internal-only options. `skipConfigParse` lets a
* trusted caller (the `aws-amplify` wrapper) that has *already* normalized the
* config via `parseAmplifyConfig` opt out of a redundant re-parse. Since
* `parseAmplifyConfig` is a no-op on an already-parsed `ResourcesConfig`, this
* only removes duplicate work (which otherwise runs on every context creation,
* and twice per SSR request through adapter-nextjs) — behavior is identical.
* @returns A branded, frozen {@link AmplifyContext}.
*
* @example
* ```ts
* import { createAmplifyContext } from '@aws-amplify/core';
*
* const ctx = createAmplifyContext(resourcesConfig, {
* Auth: { tokenProvider, credentialsProvider },
* });
* // Pass ctx explicitly to category APIs:
* await signIn(ctx, { username, password });
* ```
*/
export function createAmplifyContext(
resourceConfig: ResourcesConfig | LegacyConfig | AmplifyOutputsUnknown,
libraryOptions?: LibraryOptions,
internalOptions?: {
/** @internal Skip re-parsing an already-normalized `ResourcesConfig`. */
skipConfigParse?: boolean;
},
): AmplifyContext {
// `parseAmplifyConfig` returns an already-parsed `ResourcesConfig` unchanged,
// so skipping it when the caller has pre-parsed keeps behavior identical while
// avoiding a redundant parse.
//
// The resolved config is DEEP-frozen (matching `Amplify.configure()` in
// Amplify.ts) on BOTH paths: a shallow `Object.freeze` would leave nested
// config (e.g. `Auth.Cognito`) mutable, and on the `skipConfigParse` path the
// caller's object passes through by reference — potentially SHARED between
// several per-request contexts (adapter-nextjs reuses one parsed config for
// every request), so any nested mutation would leak across contexts.
// `deepFreeze` is idempotent, so re-freezing that shared object is safe.
const resolvedResourceConfig: ResourcesConfig = deepFreeze(
internalOptions?.skipConfigParse
? (resourceConfig as ResourcesConfig)
: parseAmplifyConfig(resourceConfig),
);
const resolvedLibraryOptions: LibraryOptions = libraryOptions ?? {};
// Fresh, per-context Auth instance (not the global singleton) so that
// multiple contexts remain isolated from one another.
//
// `configure()` is called UNCONDITIONALLY, exactly as the pre-context
// `AmplifyClass.configure()` does (see singleton/Amplify.ts). It is the only
// place `AuthClass` stores `libraryOptions.Auth`, so gating it on the
// presence of `resourcesConfig.Auth` would silently drop caller-supplied
// token/credentials providers whenever the resource config carries no `Auth`
// block — the shape `getAmplifyDataClientConfig()` produces for IAM-authed
// Data access from a Lambda (`API.GraphQL` only, plus a custom
// `credentialsProvider`). `AuthClass.configure()` tolerates an undefined
// auth resource config.
//
// The `Auth!` assertion is deliberately false: in the very case this fixes,
// `Auth` IS undefined at runtime. It mirrors the identical assertion in the
// singleton rather than widening `AuthClass.configure()`'s parameter type.
const auth = new AuthClass();
auth.configure(resolvedResourceConfig.Auth!, resolvedLibraryOptions.Auth);
const ctx: AmplifyContext = {
// Already deep-frozen above (both parse paths).
resourcesConfig: resolvedResourceConfig,
libraryOptions: resolvedLibraryOptions,
// Unique, frozen per-context identity handle (see AmplifyContextToken).
// Attached before the brand/freeze below so the frozen context carries it.
token: createAmplifyContextToken(),
fetchAuthSession: fetchOptions => auth.fetchAuthSession(fetchOptions ?? {}),
clearCredentials: () => auth.clearCredentials(),
getTokens: tokenOptions => auth.getTokens(tokenOptions),
};
// Brand the context for runtime identification by isAmplifyContext().
Object.defineProperty(ctx, AMPLIFY_CONTEXT_BRAND, {
value: true,
enumerable: false,
configurable: false,
writable: false,
});
Object.freeze(ctx);
return ctx;
}