Skip to content

Commit 2973e52

Browse files
authored
improvement(tldraw): embed config (tldraw#9068)
Closes tldraw#9024 In order to stop the default Google Maps embed from reading `process.env.NEXT_PUBLIC_GC_API_KEY` inside the SDK (tldraw#9024), this PR lets consumers pass per-embed config through `EmbedShapeUtil.configure`. Config is keyed by embed type, so each definition only receives its own values and no key leaks across embeds. ```ts EmbedShapeUtil.configure({ embedConfig: { google_maps: { apiKey: '...' } }, }) ``` tldraw.com now configures the embed util explicitly (reading the vite-inlined key in app code) instead of relying on the SDK reading the env var. ### Change type - [x] `api` ### Test plan 1. Configure `EmbedShapeUtil` with a `google_maps` api key and paste a Google Maps URL — the embed renders with the key. 2. Without config, the maps embed URL has no key (no env fallback in the SDK). - [x] Unit tests ### API changes - Added `embedConfig` option to `EmbedShapeOptions`. - Added `GoogleMapsEmbedConfig` and `DefaultEmbedConfig` types. - `EmbedDefinition`/`CustomEmbedDefinition` are now generic over a `Config`; `toEmbedUrl(url, config?)` takes an optional per-embed config. - `getEmbedInfo`/`matchUrl` accept an optional `embedConfig` argument. ### Release notes - Add `embedConfig` to `EmbedShapeUtil` so embeds like Google Maps can be given an API key via `EmbedShapeUtil.configure({ embedConfig: { google_maps: { apiKey } } })`. The default Google Maps embed no longer reads `process.env.NEXT_PUBLIC_GC_API_KEY`.
1 parent c3711ef commit 2973e52

12 files changed

Lines changed: 107 additions & 33 deletions

File tree

apps/dotcom/client/src/tla/components/TlaEditor/TlaEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { trackEvent, useHandleUiEvents } from '../../../utils/analytics'
3434
import { assetUrls } from '../../../utils/assetUrls'
3535
import { MULTIPLAYER_SERVER } from '../../../utils/config'
3636
import { createAssetFromUrl } from '../../../utils/createAssetFromUrl'
37+
import { embedShapeUtils } from '../../../utils/embedShapeUtil'
3738
import { isProductionEnv } from '../../../utils/env'
3839
import { globalEditor } from '../../../utils/globalEditor'
3940
import { multiplayerAssetStore } from '../../../utils/multiplayerAssetStore'
@@ -284,6 +285,7 @@ function TlaEditorInner({ fileSlug, deepLinks }: TlaEditorProps) {
284285
licenseKey={getLicenseKey()}
285286
store={store}
286287
assetUrls={assetUrls}
288+
shapeUtils={embedShapeUtils}
287289
user={app?.tlUser}
288290
onMount={handleMount}
289291
onUiEvent={handleUiEvent}

apps/dotcom/client/src/tla/components/TlaEditor/TlaHistorySnapshotEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ThemeUpdater } from '../../../components/ThemeUpdater/ThemeUpdater'
55
import { useLegacyUrlParams } from '../../../hooks/useLegacyUrlParams'
66
import { useHandleUiEvents } from '../../../utils/analytics'
77
import { assetUrls } from '../../../utils/assetUrls'
8+
import { embedShapeUtils } from '../../../utils/embedShapeUtil'
89
import { globalEditor } from '../../../utils/globalEditor'
910
import { useMaybeApp } from '../../hooks/useAppState'
1011
import { ReadyWrapper, useSetIsReady } from '../../hooks/useIsReady'
@@ -103,6 +104,7 @@ function TlaEditorInner({
103104
licenseKey={getLicenseKey()}
104105
snapshot={snapshot}
105106
assetUrls={assetUrls}
107+
shapeUtils={embedShapeUtils}
106108
onMount={handleMount}
107109
overrides={[fileSystemUiOverrides]}
108110
initialState={'hand'}

apps/dotcom/client/src/tla/components/TlaEditor/TlaLegacyFileEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { trackEvent, useHandleUiEvents } from '../../../utils/analytics'
1515
import { assetUrls } from '../../../utils/assetUrls'
1616
import { MULTIPLAYER_SERVER } from '../../../utils/config'
1717
import { createAssetFromUrl } from '../../../utils/createAssetFromUrl'
18+
import { embedShapeUtils } from '../../../utils/embedShapeUtil'
1819
import { globalEditor } from '../../../utils/globalEditor'
1920
import { multiplayerAssetStore } from '../../../utils/multiplayerAssetStore'
2021
import { useMaybeApp } from '../../hooks/useAppState'
@@ -115,6 +116,7 @@ function TlaEditorInner({
115116
licenseKey={getLicenseKey()}
116117
store={storeWithStatus}
117118
assetUrls={assetUrls}
119+
shapeUtils={embedShapeUtils}
118120
onMount={handleMount}
119121
overrides={[fileSystemUiOverrides]}
120122
initialState={isReadonly ? 'hand' : 'select'}

apps/dotcom/client/src/tla/components/TlaEditor/TlaLegacySnapshotEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ThemeUpdater } from '../../../components/ThemeUpdater/ThemeUpdater'
55
import { useLegacyUrlParams } from '../../../hooks/useLegacyUrlParams'
66
import { useHandleUiEvents } from '../../../utils/analytics'
77
import { assetUrls } from '../../../utils/assetUrls'
8+
import { embedShapeUtils } from '../../../utils/embedShapeUtil'
89
import { globalEditor } from '../../../utils/globalEditor'
910
import { useMaybeApp } from '../../hooks/useAppState'
1011
import { ReadyWrapper, useSetIsReady } from '../../hooks/useIsReady'
@@ -75,6 +76,7 @@ function TlaEditorInner({ snapshot }: { snapshot: TLStoreSnapshot }) {
7576
licenseKey={getLicenseKey()}
7677
snapshot={snapshot}
7778
assetUrls={assetUrls}
79+
shapeUtils={embedShapeUtils}
7880
onMount={handleMount}
7981
overrides={[fileSystemUiOverrides]}
8082
initialState={'hand'}

apps/dotcom/client/src/tla/components/TlaEditor/TlaPublishEditor.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useLegacyUrlParams } from '../../../hooks/useLegacyUrlParams'
66
import { usePerformanceTracking } from '../../../hooks/usePerformanceTracking'
77
import { useHandleUiEvents } from '../../../utils/analytics'
88
import { assetUrls } from '../../../utils/assetUrls'
9+
import { embedShapeUtils } from '../../../utils/embedShapeUtil'
910
import { globalEditor } from '../../../utils/globalEditor'
1011
import { TlaEditorErrorFallback } from './editor-components/TlaEditorErrorFallback'
1112
import { TlaEditorPublishedSharePanel } from './editor-components/TlaEditorPublishedSharePanel'
@@ -48,6 +49,7 @@ export function TlaPublishEditor({ schema, records }: TlaPublishEditorProps) {
4849
<Tldraw
4950
licenseKey={getLicenseKey()}
5051
assetUrls={assetUrls}
52+
shapeUtils={embedShapeUtils}
5153
snapshot={snapshot}
5254
overrides={[fileEditorOverrides]}
5355
onUiEvent={handleUiEvent}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { EmbedShapeUtil } from 'tldraw'
2+
3+
// vite inlines this NEXT_PUBLIC_ var at build time.
4+
const googleMapsApiKey = process.env.NEXT_PUBLIC_GC_API_KEY
5+
6+
// Module-scoped so the array keeps a stable identity across renders.
7+
export const embedShapeUtils = [
8+
EmbedShapeUtil.configure({ embedConfig: { google_maps: { apiKey: googleMapsApiKey } } }),
9+
]

packages/tldraw/api-report.api.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -851,7 +851,7 @@ export interface CustomDebugFlags {
851851
}
852852

853853
// @public (undocumented)
854-
export interface CustomEmbedDefinition extends EmbedDefinition {
854+
export interface CustomEmbedDefinition<Config = never> extends EmbedDefinition<Config> {
855855
// (undocumented)
856856
readonly icon: string;
857857
}
@@ -928,7 +928,7 @@ export const DEFAULT_EMBED_DEFINITIONS: readonly [{
928928
readonly 'allow-presentation': true;
929929
};
930930
readonly title: "Google Maps";
931-
readonly toEmbedUrl: (url: string) => string | undefined;
931+
readonly toEmbedUrl: (url: string, config?: GoogleMapsEmbedConfig | undefined) => string | undefined;
932932
readonly type: "google_maps";
933933
readonly width: 720;
934934
}, {
@@ -1144,6 +1144,12 @@ export const DefaultDialogs: NamedExoticComponent<object>;
11441144
// @public (undocumented)
11451145
export let defaultEditorAssetUrls: TLEditorAssetUrls;
11461146

1147+
// @public
1148+
export interface DefaultEmbedConfig {
1149+
// (undocumented)
1150+
readonly google_maps?: GoogleMapsEmbedConfig;
1151+
}
1152+
11471153
// @public (undocumented)
11481154
export type DefaultEmbedDefinitionType = (typeof DEFAULT_EMBED_DEFINITIONS)[number]['type'];
11491155

@@ -1749,7 +1755,7 @@ export interface ElbowArrowTargetBox extends ElbowArrowBox {
17491755
export function EllipseToolbarItem(): JSX.Element;
17501756

17511757
// @public (undocumented)
1752-
export interface EmbedDefinition {
1758+
export interface EmbedDefinition<Config = never> {
17531759
// (undocumented)
17541760
readonly backgroundColor?: string;
17551761
// (undocumented)
@@ -1779,7 +1785,7 @@ export interface EmbedDefinition {
17791785
// (undocumented)
17801786
readonly title: string;
17811787
// (undocumented)
1782-
readonly toEmbedUrl: (url: string) => string | undefined;
1788+
readonly toEmbedUrl: (url: string, config?: Config) => string | undefined;
17831789
// (undocumented)
17841790
readonly type: string;
17851791
// (undocumented)
@@ -1788,6 +1794,7 @@ export interface EmbedDefinition {
17881794

17891795
// @public (undocumented)
17901796
export interface EmbedShapeOptions extends ShapeOptionsWithDisplayValues<TLEmbedShape, EmbedShapeUtilDisplayValues> {
1797+
readonly embedConfig?: DefaultEmbedConfig & Record<string, unknown>;
17911798
readonly embedDefinitions: readonly TLEmbedDefinition[];
17921799
}
17931800

@@ -1850,7 +1857,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
18501857
// (undocumented)
18511858
static props: RecordProps<TLEmbedShape>;
18521859
// @deprecated (undocumented)
1853-
static setEmbedDefinitions(embedDefinitions: readonly EmbedDefinition[]): void;
1860+
static setEmbedDefinitions(embedDefinitions: readonly TLEmbedDefinition[]): void;
18541861
// (undocumented)
18551862
static type: "embed";
18561863
}
@@ -2348,7 +2355,7 @@ export function getDisplayValues<Shape extends TLShape, DisplayValues extends ob
23482355
}, shape: Shape, colorMode?: 'dark' | 'light'): DisplayValues;
23492356

23502357
// @public
2351-
export function getEmbedInfo(definitions: readonly TLEmbedDefinition[], inputUrl: string): TLEmbedResult;
2358+
export function getEmbedInfo(definitions: readonly TLEmbedDefinition[], inputUrl: string, embedConfig?: Record<string, unknown>): TLEmbedResult;
23522359

23532360
// @public (undocumented)
23542361
export function getFontFamily(theme: TLTheme, font: string): string;
@@ -2389,6 +2396,12 @@ export function getUncroppedSize(shapeSize: {
23892396
w: number;
23902397
};
23912398

2399+
// @public
2400+
export interface GoogleMapsEmbedConfig {
2401+
// (undocumented)
2402+
readonly apiKey?: string;
2403+
}
2404+
23922405
// @public (undocumented)
23932406
export function GroupMenuItem(): JSX.Element | null;
23942407

@@ -4428,7 +4441,7 @@ export interface TLElbowArrowInfo {
44284441
}
44294442

44304443
// @public (undocumented)
4431-
export type TLEmbedDefinition = CustomEmbedDefinition | EmbedDefinition;
4444+
export type TLEmbedDefinition = CustomEmbedDefinition<any> | EmbedDefinition<any>;
44324445

44334446
// @public (undocumented)
44344447
export type TLEmbedResult = {

packages/tldraw/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,10 @@ export {
116116
embedShapePermissionDefaults,
117117
unknownEmbedShapePermissionOverrides,
118118
type CustomEmbedDefinition,
119+
type DefaultEmbedConfig,
119120
type DefaultEmbedDefinitionType,
120121
type EmbedDefinition,
122+
type GoogleMapsEmbedConfig,
121123
type TLEmbedDefinition,
122124
type TLEmbedShapePermissions,
123125
} from './lib/defaultEmbedDefinitions'

packages/tldraw/src/lib/defaultEmbedDefinitions.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
111111
overridePermissions: {
112112
'allow-presentation': true,
113113
},
114-
toEmbedUrl: (url) => {
114+
toEmbedUrl: (url, config?: GoogleMapsEmbedConfig) => {
115115
if (url.includes('/maps/embed?')) {
116116
return url
117117
} else if (url.includes('/maps/')) {
@@ -126,7 +126,9 @@ export const DEFAULT_EMBED_DEFINITIONS = [
126126
? zoomOrMeters
127127
: -Math.log2(parseInt(zoomOrMeters) / 14772321) / 0.8
128128
const host = new URL(url).host.replace('www.', '')
129-
result = `https://${host}/maps/embed/v1/view?key=${process.env.NEXT_PUBLIC_GC_API_KEY}&center=${lat},${lng}&zoom=${z}&maptype=${mapType}`
129+
// TODO: remove the process.env fallback once all consumers pass `apiKey` via `embedConfig`.
130+
const apiKey = config?.apiKey ?? process.env.NEXT_PUBLIC_GC_API_KEY
131+
result = `https://${host}/maps/embed/v1/view?key=${apiKey}&center=${lat},${lng}&zoom=${z}&maptype=${mapType}`
130132
} else {
131133
result = ''
132134
}
@@ -702,8 +704,28 @@ export const unknownEmbedShapePermissionOverrides: TLEmbedShapePermissions = {
702704
'allow-popups': false,
703705
}
704706

707+
/**
708+
* Configuration for the default Google Maps embed. Provide an `apiKey` through
709+
* {@link EmbedShapeUtil}'s `embedConfig` option to render Google Maps embeds.
710+
*
711+
* @public
712+
*/
713+
export interface GoogleMapsEmbedConfig {
714+
readonly apiKey?: string
715+
}
716+
717+
/**
718+
* Per-embed configuration for the default embed definitions, keyed by embed type.
719+
* Passed to an embed definition's `toEmbedUrl` when building its embed URL.
720+
*
721+
* @public
722+
*/
723+
export interface DefaultEmbedConfig {
724+
readonly google_maps?: GoogleMapsEmbedConfig
725+
}
726+
705727
/** @public */
706-
export interface EmbedDefinition {
728+
export interface EmbedDefinition<Config = never> {
707729
readonly type: string
708730
readonly title: string
709731
readonly hostnames: readonly string[]
@@ -721,18 +743,18 @@ export interface EmbedDefinition {
721743
// TODO: FIXME this is ugly be required because some embeds have their own border radius for example spotify embeds
722744
readonly overrideOutlineRadius?: number
723745
// eslint-disable-next-line tldraw/method-signature-style
724-
readonly toEmbedUrl: (url: string) => string | undefined
746+
readonly toEmbedUrl: (url: string, config?: Config) => string | undefined
725747
// eslint-disable-next-line tldraw/method-signature-style
726748
readonly fromEmbedUrl: (url: string) => string | undefined
727749
}
728750

729751
/** @public */
730-
export interface CustomEmbedDefinition extends EmbedDefinition {
752+
export interface CustomEmbedDefinition<Config = never> extends EmbedDefinition<Config> {
731753
readonly icon: string
732754
}
733755

734756
/** @public */
735-
export type TLEmbedDefinition = EmbedDefinition | CustomEmbedDefinition
757+
export type TLEmbedDefinition = EmbedDefinition<any> | CustomEmbedDefinition<any>
736758

737759
/** @public */
738760
export type DefaultEmbedDefinitionType = (typeof DEFAULT_EMBED_DEFINITIONS)[number]['type']

packages/tldraw/src/lib/shapes/embed/EmbedShapeUtil.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from '@tldraw/editor'
1919
import {
2020
DEFAULT_EMBED_DEFINITIONS,
21-
EmbedDefinition,
21+
DefaultEmbedConfig,
2222
TLEmbedDefinition,
2323
TLEmbedShapePermissions,
2424
embedShapePermissionDefaults,
@@ -41,6 +41,15 @@ export interface EmbedShapeOptions extends ShapeOptionsWithDisplayValues<
4141
> {
4242
/** The embed definitions to use for this shape util. */
4343
readonly embedDefinitions: readonly TLEmbedDefinition[]
44+
/**
45+
* Per-embed configuration, keyed by embed type. Passed to each definition's `toEmbedUrl` when
46+
* building its embed URL — for example, an API key for the default Google Maps embed:
47+
*
48+
* ```ts
49+
* EmbedShapeUtil.configure({ embedConfig: { google_maps: { apiKey: '...' } } })
50+
* ```
51+
*/
52+
readonly embedConfig?: DefaultEmbedConfig & Record<string, unknown>
4453
}
4554

4655
const getSandboxPermissions = (permissions: TLEmbedShapePermissions) => {
@@ -58,6 +67,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
5867

5968
override options: EmbedShapeOptions = {
6069
embedDefinitions: DEFAULT_EMBED_DEFINITIONS,
70+
embedConfig: {},
6171
getDefaultDisplayValues(): EmbedShapeUtilDisplayValues {
6272
return {
6373
showShadow: true,
@@ -74,10 +84,10 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
7484
return result.definition.canEditWhileLocked ?? true
7585
}
7686

77-
private static legacyEmbedDefinitions: readonly EmbedDefinition[] | null = null
87+
private static legacyEmbedDefinitions: readonly TLEmbedDefinition[] | null = null
7888

7989
/** @deprecated - Use `EmbedShapeUtil.configure({ embedDefinitions: [...] })` instead. */
80-
static setEmbedDefinitions(embedDefinitions: readonly EmbedDefinition[]) {
90+
static setEmbedDefinitions(embedDefinitions: readonly TLEmbedDefinition[]) {
8191
EmbedShapeUtil.legacyEmbedDefinitions = embedDefinitions
8292
}
8393

@@ -90,7 +100,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
90100
}
91101

92102
getEmbedDefinition(url: string): TLEmbedResult {
93-
return getEmbedInfo(this.getEmbedDefs(), url)
103+
return getEmbedInfo(this.getEmbedDefs(), url, this.options.embedConfig)
94104
}
95105

96106
override getText(shape: TLEmbedShape) {

0 commit comments

Comments
 (0)