@tinyweb_dev/oe-meeting-react 0.1.0 → 0.3.0

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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/OeMeeting.tsx","../src/embed-protocol.ts","../src/buildEmbedUrl.ts","../src/useOeMeeting.ts","../src/parseEmbedEvent.ts"],"sourcesContent":["'use client';\n\nimport {\n forwardRef,\n useId,\n useMemo,\n type CSSProperties,\n} from 'react';\nimport { buildEmbedUrl } from './buildEmbedUrl';\nimport { DEFAULT_EMBED_ORIGIN } from './embed-protocol';\nimport type { OeMeetingHandle, OeMeetingProps } from './types';\nimport { useOeMeeting } from './useOeMeeting';\n\nexport const DEFAULT_IFRAME_ALLOW =\n 'camera; microphone; fullscreen; autoplay; display-capture; clipboard-write; encrypted-media';\n\nfunction toCssSize(value: number | string | undefined): string | undefined {\n if (value === undefined) return undefined;\n return typeof value === 'number' ? `${value}px` : value;\n}\n\nfunction getOrigin(value: string): string {\n return new URL(value).origin;\n}\n\nexport const OeMeeting = forwardRef<OeMeetingHandle, OeMeetingProps>(\n function OeMeeting(props, ref) {\n const {\n roomSlug,\n joinToken,\n userName,\n externalUserId,\n origin = DEFAULT_EMBED_ORIGIN,\n lang,\n autoJoin = true,\n hideLeave,\n title = 'OE Meeting room',\n width = '100%',\n height = 640,\n className,\n style,\n iframeClassName,\n iframeStyle,\n allow = DEFAULT_IFRAME_ALLOW,\n targetOrigin,\n onReady,\n onJoined,\n onLeft,\n onParticipantJoined,\n onParticipantLeft,\n onEnded,\n onError,\n onEvent,\n } = props;\n const reactId = useId();\n const instanceId = useMemo(\n () => `oem-${reactId.replace(/:/g, '')}`,\n [reactId],\n );\n const embedOrigin = useMemo(() => getOrigin(origin), [origin]);\n const resolvedTargetOrigin = targetOrigin ?? embedOrigin;\n const hostOrigin =\n typeof window === 'undefined' ? undefined : window.location.origin;\n const src = useMemo(\n () =>\n buildEmbedUrl({\n roomSlug,\n joinToken,\n userName,\n externalUserId,\n origin,\n lang,\n autoJoin,\n hideLeave,\n instanceId,\n hostOrigin,\n }),\n [\n autoJoin,\n externalUserId,\n hideLeave,\n hostOrigin,\n instanceId,\n joinToken,\n lang,\n origin,\n roomSlug,\n userName,\n ],\n );\n const { iframeRef } = useOeMeeting({\n instanceId,\n targetOrigin: resolvedTargetOrigin,\n handleRef: ref,\n onReady,\n onJoined,\n onLeft,\n onParticipantJoined,\n onParticipantLeft,\n onEnded,\n onError,\n onEvent,\n });\n const wrapperStyle: CSSProperties = {\n width: toCssSize(width),\n height: toCssSize(height),\n ...style,\n };\n const frameStyle: CSSProperties = {\n width: '100%',\n height: '100%',\n border: 0,\n display: 'block',\n ...iframeStyle,\n };\n\n return (\n <div\n className={className}\n style={wrapperStyle}\n data-oe-meeting-instance={instanceId}\n >\n <iframe\n ref={iframeRef}\n className={iframeClassName}\n style={frameStyle}\n src={src}\n title={title}\n allow={allow}\n allowFullScreen\n referrerPolicy=\"strict-origin-when-cross-origin\"\n />\n </div>\n );\n },\n);\n\nOeMeeting.displayName = 'OeMeeting';\n","export const EMBED_PROTOCOL_VERSION = 1 as const;\nexport const EMBED_MESSAGE_SOURCE = 'oemeeting' as const;\nexport const DEFAULT_EMBED_ORIGIN = 'https://meet.oceanedu.org';\n\nexport enum EmbedEventType {\n READY = 'oemeeting:ready',\n JOINED = 'oemeeting:joined',\n LEFT = 'oemeeting:left',\n PARTICIPANT_JOINED = 'oemeeting:participant-joined',\n PARTICIPANT_LEFT = 'oemeeting:participant-left',\n ENDED = 'oemeeting:ended',\n ERROR = 'oemeeting:error',\n}\n\nexport enum EmbedCommandType {\n COMMAND = 'oemeeting:command',\n}\n\nexport enum EmbedCommandAction {\n LEAVE = 'leave',\n TOGGLE_MIC = 'toggleMic',\n TOGGLE_CAMERA = 'toggleCamera',\n}\n\nexport enum EmbedErrorCode {\n TOKEN_INVALID = 'TOKEN_INVALID',\n TOKEN_EXPIRED = 'TOKEN_EXPIRED',\n ROOM_NOT_FOUND = 'ROOM_NOT_FOUND',\n ROOM_ACCESS_DENIED = 'ROOM_ACCESS_DENIED',\n CONNECTION_FAILED = 'CONNECTION_FAILED',\n PROTOCOL_MISMATCH = 'PROTOCOL_MISMATCH',\n UNKNOWN = 'UNKNOWN',\n}\n\nexport enum EmbedQueryParam {\n INSTANCE_ID = 'instanceId',\n HOST_ORIGIN = 'hostOrigin',\n JOIN_TOKEN = 'joinToken',\n USER_NAME = 'userName',\n EXTERNAL_USER_ID = 'externalUserId',\n LANG = 'lang',\n AUTO_JOIN = 'autoJoin',\n HIDE_LEAVE = 'hideLeave',\n}\n","import {\n DEFAULT_EMBED_ORIGIN,\n EmbedQueryParam,\n} from './embed-protocol';\nimport type { BuildEmbedUrlOptions } from './types';\n\nconst QUERY_FLAG_ENABLED = '1';\nconst QUERY_FLAG_DISABLED = '0';\n\nfunction toFlag(value: boolean | undefined): string | undefined {\n if (value === undefined) return undefined;\n return value ? QUERY_FLAG_ENABLED : QUERY_FLAG_DISABLED;\n}\n\nfunction normalizeOrigin(origin: string): string {\n return origin.replace(/\\/+$/, '');\n}\n\nexport function buildEmbedUrl(options: BuildEmbedUrlOptions): string {\n const roomSlug = options.roomSlug.trim();\n const joinToken = options.joinToken.trim();\n if (!roomSlug) {\n throw new Error('OE Meeting embed requires a non-empty roomSlug.');\n }\n if (!joinToken) {\n throw new Error('OE Meeting embed requires a non-empty joinToken.');\n }\n\n const origin = normalizeOrigin(options.origin ?? DEFAULT_EMBED_ORIGIN);\n const url = new URL(`${origin}/embed/${encodeURIComponent(roomSlug)}`);\n const entries: Array<[EmbedQueryParam, string | undefined]> = [\n [EmbedQueryParam.INSTANCE_ID, options.instanceId],\n [EmbedQueryParam.HOST_ORIGIN, options.hostOrigin],\n [EmbedQueryParam.JOIN_TOKEN, joinToken],\n [EmbedQueryParam.USER_NAME, options.userName],\n [EmbedQueryParam.EXTERNAL_USER_ID, options.externalUserId],\n [EmbedQueryParam.LANG, options.lang],\n [EmbedQueryParam.AUTO_JOIN, toFlag(options.autoJoin)],\n [EmbedQueryParam.HIDE_LEAVE, toFlag(options.hideLeave)],\n ];\n\n for (const [key, value] of entries) {\n if (value !== undefined && value !== '') {\n url.searchParams.set(key, value);\n }\n }\n return url.toString();\n}\n","import {\n useCallback,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n type Ref,\n} from 'react';\nimport {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n EmbedCommandAction,\n EmbedCommandType,\n EmbedEventType,\n} from './embed-protocol';\nimport { parseEmbedEvent } from './parseEmbedEvent';\nimport type {\n EmbedCommandMessage,\n OeMeetingHandle,\n OeMeetingProps,\n} from './types';\n\ntype UseOeMeetingOptions = Pick<\n OeMeetingProps,\n | 'onReady'\n | 'onJoined'\n | 'onLeft'\n | 'onParticipantJoined'\n | 'onParticipantLeft'\n | 'onEnded'\n | 'onError'\n | 'onEvent'\n> & {\n instanceId: string;\n targetOrigin: string;\n handleRef?: Ref<OeMeetingHandle>;\n};\n\ninterface PostCommandOptions {\n iframe: HTMLIFrameElement | null;\n instanceId: string;\n action: EmbedCommandAction;\n targetOrigin: string;\n}\n\nfunction postCommand(options: PostCommandOptions): void {\n const frameWindow = options.iframe?.contentWindow;\n if (!frameWindow) return;\n const message: EmbedCommandMessage = {\n source: EMBED_MESSAGE_SOURCE,\n version: EMBED_PROTOCOL_VERSION,\n instanceId: options.instanceId,\n type: EmbedCommandType.COMMAND,\n action: options.action,\n };\n frameWindow.postMessage(message, options.targetOrigin);\n}\n\nexport function useOeMeeting(options: UseOeMeetingOptions) {\n const iframeRef = useRef<HTMLIFrameElement | null>(null);\n const callbacksRef = useRef(options);\n\n useEffect(() => {\n callbacksRef.current = options;\n }, [options]);\n\n useEffect(() => {\n const onMessage = (event: MessageEvent<unknown>) => {\n const frameWindow = iframeRef.current?.contentWindow;\n if (\n !frameWindow ||\n event.source !== frameWindow ||\n event.origin !== options.targetOrigin\n ) {\n return;\n }\n const embedEvent = parseEmbedEvent(event.data);\n if (!embedEvent || embedEvent.instanceId !== options.instanceId) return;\n\n const callbacks = callbacksRef.current;\n callbacks.onEvent?.(embedEvent);\n switch (embedEvent.type) {\n case EmbedEventType.READY:\n callbacks.onReady?.(embedEvent);\n break;\n case EmbedEventType.JOINED:\n callbacks.onJoined?.(embedEvent);\n break;\n case EmbedEventType.LEFT:\n callbacks.onLeft?.(embedEvent);\n break;\n case EmbedEventType.PARTICIPANT_JOINED:\n callbacks.onParticipantJoined?.(embedEvent);\n break;\n case EmbedEventType.PARTICIPANT_LEFT:\n callbacks.onParticipantLeft?.(embedEvent);\n break;\n case EmbedEventType.ENDED:\n callbacks.onEnded?.(embedEvent);\n break;\n case EmbedEventType.ERROR:\n callbacks.onError?.(embedEvent);\n break;\n }\n };\n\n window.addEventListener('message', onMessage);\n return () => window.removeEventListener('message', onMessage);\n }, [options.instanceId, options.targetOrigin]);\n\n const send = useCallback(\n (action: EmbedCommandAction) =>\n postCommand({\n iframe: iframeRef.current,\n instanceId: options.instanceId,\n action,\n targetOrigin: options.targetOrigin,\n }),\n [options.instanceId, options.targetOrigin],\n );\n const leave = useCallback(() => send(EmbedCommandAction.LEAVE), [send]);\n const toggleMic = useCallback(\n () => send(EmbedCommandAction.TOGGLE_MIC),\n [send],\n );\n const toggleCamera = useCallback(\n () => send(EmbedCommandAction.TOGGLE_CAMERA),\n [send],\n );\n const getIframe = useCallback(() => iframeRef.current, []);\n const handle = useMemo<OeMeetingHandle>(\n () => ({ leave, toggleMic, toggleCamera, getIframe }),\n [getIframe, leave, toggleCamera, toggleMic],\n );\n\n useImperativeHandle(options.handleRef, () => handle, [handle]);\n return { iframeRef, handle };\n}\n","import {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n EmbedEventType,\n} from './embed-protocol';\nimport type { EmbedEvent } from './types';\n\nconst EVENT_TYPES = new Set<string>(Object.values(EmbedEventType));\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isOptionalString(value: unknown): boolean {\n return value === undefined || typeof value === 'string';\n}\n\nfunction hasValidPayload(\n type: EmbedEventType,\n payload: Record<string, unknown>,\n): boolean {\n switch (type) {\n case EmbedEventType.READY:\n return typeof payload.roomSlug === 'string';\n case EmbedEventType.JOINED:\n return (\n typeof payload.roomSlug === 'string' &&\n typeof payload.identity === 'string' &&\n isOptionalString(payload.name) &&\n typeof payload.joinedAt === 'string'\n );\n case EmbedEventType.LEFT:\n return (\n typeof payload.roomSlug === 'string' &&\n typeof payload.identity === 'string' &&\n typeof payload.leftAt === 'string' &&\n (payload.durationMs === undefined ||\n typeof payload.durationMs === 'number')\n );\n case EmbedEventType.PARTICIPANT_JOINED:\n return (\n typeof payload.identity === 'string' &&\n isOptionalString(payload.name) &&\n typeof payload.participantCount === 'number'\n );\n case EmbedEventType.PARTICIPANT_LEFT:\n return (\n typeof payload.identity === 'string' &&\n typeof payload.participantCount === 'number'\n );\n case EmbedEventType.ENDED:\n return (\n typeof payload.roomSlug === 'string' &&\n typeof payload.endedAt === 'string'\n );\n case EmbedEventType.ERROR:\n return (\n typeof payload.code === 'string' &&\n typeof payload.message === 'string'\n );\n }\n}\n\nexport function parseEmbedEvent(data: unknown): EmbedEvent | null {\n if (!isRecord(data)) return null;\n if (\n data.source !== EMBED_MESSAGE_SOURCE ||\n data.version !== EMBED_PROTOCOL_VERSION ||\n typeof data.instanceId !== 'string' ||\n data.instanceId.length === 0 ||\n typeof data.type !== 'string' ||\n !EVENT_TYPES.has(data.type) ||\n !isRecord(data.payload)\n ) {\n return null;\n }\n\n const type = data.type as EmbedEventType;\n if (!hasValidPayload(type, data.payload)) return null;\n return data as unknown as EmbedEvent;\n}\n"],"mappings":"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","names":["useMemo","EmbedEventType","EmbedCommandType","EmbedCommandAction","EmbedErrorCode","EmbedQueryParam","OeMeeting","useMemo"]}
1
+ {"version":3,"sources":["../src/OeMeeting.tsx","../src/embed-protocol.ts","../src/buildEmbedUrl.ts","../src/useOeMeeting.ts","../src/openStage.ts","../src/parseEmbedEvent.ts"],"sourcesContent":["'use client';\n\nimport {\n forwardRef,\n useId,\n useMemo,\n type CSSProperties,\n} from 'react';\nimport { buildEmbedUrl } from './buildEmbedUrl';\nimport { DEFAULT_EMBED_ORIGIN } from './embed-protocol';\nimport type { OeMeetingHandle, OeMeetingProps } from './types';\nimport { useOeMeeting } from './useOeMeeting';\n\nexport const DEFAULT_IFRAME_ALLOW =\n 'camera; microphone; fullscreen; autoplay; display-capture; clipboard-write; encrypted-media';\n\nfunction toCssSize(value: number | string | undefined): string | undefined {\n if (value === undefined) return undefined;\n return typeof value === 'number' ? `${value}px` : value;\n}\n\nfunction getOrigin(value: string): string {\n return new URL(value).origin;\n}\n\nexport const OeMeeting = forwardRef<OeMeetingHandle, OeMeetingProps>(\n function OeMeeting(props, ref) {\n const {\n roomSlug,\n joinToken,\n userName,\n externalUserId,\n origin = DEFAULT_EMBED_ORIGIN,\n lang,\n autoJoin = true,\n hideLeave,\n autoTranscribe,\n autoRecord,\n title = 'OE Meeting room',\n width = '100%',\n height = 640,\n className,\n style,\n iframeClassName,\n iframeStyle,\n allow = DEFAULT_IFRAME_ALLOW,\n targetOrigin,\n onReady,\n onJoined,\n onLeft,\n onParticipantJoined,\n onParticipantLeft,\n onEnded,\n onError,\n onStageChanged,\n onTranscriptStarted,\n onRecordingStarted,\n onEvent,\n } = props;\n const reactId = useId();\n const instanceId = useMemo(\n () => `oem-${reactId.replace(/:/g, '')}`,\n [reactId],\n );\n const embedOrigin = useMemo(() => getOrigin(origin), [origin]);\n const resolvedTargetOrigin = targetOrigin ?? embedOrigin;\n const hostOrigin =\n typeof window === 'undefined' ? undefined : window.location.origin;\n const src = useMemo(\n () =>\n buildEmbedUrl({\n roomSlug,\n joinToken,\n userName,\n externalUserId,\n origin,\n lang,\n autoJoin,\n hideLeave,\n autoTranscribe,\n autoRecord,\n instanceId,\n hostOrigin,\n }),\n [\n autoJoin,\n autoRecord,\n autoTranscribe,\n externalUserId,\n hideLeave,\n hostOrigin,\n instanceId,\n joinToken,\n lang,\n origin,\n roomSlug,\n userName,\n ],\n );\n const { iframeRef } = useOeMeeting({\n instanceId,\n targetOrigin: resolvedTargetOrigin,\n handleRef: ref,\n onReady,\n onJoined,\n onLeft,\n onParticipantJoined,\n onParticipantLeft,\n onEnded,\n onError,\n onStageChanged,\n onTranscriptStarted,\n onRecordingStarted,\n onEvent,\n });\n const wrapperStyle: CSSProperties = {\n width: toCssSize(width),\n height: toCssSize(height),\n ...style,\n };\n const frameStyle: CSSProperties = {\n width: '100%',\n height: '100%',\n border: 0,\n display: 'block',\n ...iframeStyle,\n };\n\n return (\n <div\n className={className}\n style={wrapperStyle}\n data-oe-meeting-instance={instanceId}\n >\n <iframe\n ref={iframeRef}\n className={iframeClassName}\n style={frameStyle}\n src={src}\n title={title}\n allow={allow}\n allowFullScreen\n referrerPolicy=\"strict-origin-when-cross-origin\"\n />\n </div>\n );\n },\n);\n\nOeMeeting.displayName = 'OeMeeting';\n","export const EMBED_PROTOCOL_VERSION = 1 as const;\nexport const EMBED_MESSAGE_SOURCE = 'oemeeting' as const;\nexport const DEFAULT_EMBED_ORIGIN = 'https://meet.ocean.edu.vn';\n\nexport enum EmbedEventType {\n READY = 'oemeeting:ready',\n JOINED = 'oemeeting:joined',\n LEFT = 'oemeeting:left',\n PARTICIPANT_JOINED = 'oemeeting:participant-joined',\n PARTICIPANT_LEFT = 'oemeeting:participant-left',\n ENDED = 'oemeeting:ended',\n ERROR = 'oemeeting:error',\n STAGE_CHANGED = 'oemeeting:stage-changed',\n TRANSCRIPT_STARTED = 'oemeeting:transcript-started',\n RECORDING_STARTED = 'oemeeting:recording-started',\n}\n\nexport enum EmbedCommandType {\n COMMAND = 'oemeeting:command',\n}\n\nexport enum EmbedCommandAction {\n LEAVE = 'leave',\n TOGGLE_MIC = 'toggleMic',\n TOGGLE_CAMERA = 'toggleCamera',\n OPEN_STAGE = 'openStage',\n CLOSE_STAGE = 'closeStage',\n}\n\nexport enum EmbedErrorCode {\n TOKEN_INVALID = 'TOKEN_INVALID',\n TOKEN_EXPIRED = 'TOKEN_EXPIRED',\n ROOM_NOT_FOUND = 'ROOM_NOT_FOUND',\n ROOM_ACCESS_DENIED = 'ROOM_ACCESS_DENIED',\n CONNECTION_FAILED = 'CONNECTION_FAILED',\n PROTOCOL_MISMATCH = 'PROTOCOL_MISMATCH',\n STAGE_ORIGIN_DENIED = 'STAGE_ORIGIN_DENIED',\n STAGE_URL_INVALID = 'STAGE_URL_INVALID',\n TRANSCRIPT_START_FAILED = 'TRANSCRIPT_START_FAILED',\n RECORDING_START_FAILED = 'RECORDING_START_FAILED',\n UNKNOWN = 'UNKNOWN',\n}\n\n/** Open host-owned tool id. Meeting does not catalog values. */\nexport type StageToolId = string;\n\nexport const MAX_STAGE_URL_LENGTH = 2048;\nexport const MAX_STAGE_TOOL_ID_LENGTH = 32;\n\nconst STAGE_TOOL_ID_PATTERN = /^[a-z][a-z0-9._-]{0,31}$/;\n\nexport function isStageToolId(value: unknown): value is StageToolId {\n return (\n typeof value === 'string' &&\n value.length > 0 &&\n value.length <= MAX_STAGE_TOOL_ID_LENGTH &&\n STAGE_TOOL_ID_PATTERN.test(value)\n );\n}\n\nexport enum EmbedQueryParam {\n INSTANCE_ID = 'instanceId',\n HOST_ORIGIN = 'hostOrigin',\n JOIN_TOKEN = 'joinToken',\n USER_NAME = 'userName',\n EXTERNAL_USER_ID = 'externalUserId',\n LANG = 'lang',\n AUTO_JOIN = 'autoJoin',\n HIDE_LEAVE = 'hideLeave',\n AUTO_TRANSCRIBE = 'autoTranscribe',\n AUTO_RECORD = 'autoRecord',\n}\n\n/**\n * What happened to the auto-transcribe request the embed made.\n *\n * `ALREADY_RUNNING` is the normal outcome for every participant after the\n * first, because the transcript is a room-level asset rather than a\n * per-participant one. `DISABLED` means the OE Meeting deployment has\n * transcription turned off.\n */\nexport enum EmbedCaptureOutcome {\n STARTED = 'started',\n ALREADY_RUNNING = 'already_running',\n DISABLED = 'disabled',\n FAILED = 'failed',\n}\n","import {\n DEFAULT_EMBED_ORIGIN,\n EmbedQueryParam,\n} from './embed-protocol';\nimport type { BuildEmbedUrlOptions } from './types';\n\nconst QUERY_FLAG_ENABLED = '1';\nconst QUERY_FLAG_DISABLED = '0';\n\nfunction toFlag(value: boolean | undefined): string | undefined {\n if (value === undefined) return undefined;\n return value ? QUERY_FLAG_ENABLED : QUERY_FLAG_DISABLED;\n}\n\nfunction normalizeOrigin(origin: string): string {\n return origin.replace(/\\/+$/, '');\n}\n\nexport function buildEmbedUrl(options: BuildEmbedUrlOptions): string {\n const roomSlug = options.roomSlug.trim();\n const joinToken = options.joinToken.trim();\n if (!roomSlug) {\n throw new Error('OE Meeting embed requires a non-empty roomSlug.');\n }\n if (!joinToken) {\n throw new Error('OE Meeting embed requires a non-empty joinToken.');\n }\n\n const origin = normalizeOrigin(options.origin ?? DEFAULT_EMBED_ORIGIN);\n const url = new URL(`${origin}/embed/${encodeURIComponent(roomSlug)}`);\n const entries: Array<[EmbedQueryParam, string | undefined]> = [\n [EmbedQueryParam.INSTANCE_ID, options.instanceId],\n [EmbedQueryParam.HOST_ORIGIN, options.hostOrigin],\n [EmbedQueryParam.JOIN_TOKEN, joinToken],\n [EmbedQueryParam.USER_NAME, options.userName],\n [EmbedQueryParam.EXTERNAL_USER_ID, options.externalUserId],\n [EmbedQueryParam.LANG, options.lang],\n [EmbedQueryParam.AUTO_JOIN, toFlag(options.autoJoin)],\n [EmbedQueryParam.HIDE_LEAVE, toFlag(options.hideLeave)],\n [EmbedQueryParam.AUTO_TRANSCRIBE, toFlag(options.autoTranscribe)],\n [EmbedQueryParam.AUTO_RECORD, toFlag(options.autoRecord)],\n ];\n\n for (const [key, value] of entries) {\n if (value !== undefined && value !== '') {\n url.searchParams.set(key, value);\n }\n }\n return url.toString();\n}\n","import {\n useCallback,\n useEffect,\n useImperativeHandle,\n useMemo,\n useRef,\n type Ref,\n} from 'react';\nimport {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n EmbedCommandAction,\n EmbedCommandType,\n EmbedEventType,\n} from './embed-protocol';\nimport { normalizeOpenStageInput } from './openStage';\nimport { parseEmbedEvent } from './parseEmbedEvent';\nimport type {\n EmbedCommandMessage,\n OeMeetingHandle,\n OeMeetingProps,\n OpenStageInput,\n} from './types';\n\ntype UseOeMeetingOptions = Pick<\n OeMeetingProps,\n | 'onReady'\n | 'onJoined'\n | 'onLeft'\n | 'onParticipantJoined'\n | 'onParticipantLeft'\n | 'onEnded'\n | 'onError'\n | 'onStageChanged'\n | 'onTranscriptStarted'\n | 'onRecordingStarted'\n | 'onEvent'\n> & {\n instanceId: string;\n targetOrigin: string;\n handleRef?: Ref<OeMeetingHandle>;\n};\n\ninterface PostCommandOptions {\n iframe: HTMLIFrameElement | null;\n instanceId: string;\n action: EmbedCommandAction;\n targetOrigin: string;\n payload?: OpenStageInput;\n}\n\nfunction postCommand(options: PostCommandOptions): void {\n const frameWindow = options.iframe?.contentWindow;\n if (!frameWindow) return;\n const message: EmbedCommandMessage = {\n source: EMBED_MESSAGE_SOURCE,\n version: EMBED_PROTOCOL_VERSION,\n instanceId: options.instanceId,\n type: EmbedCommandType.COMMAND,\n action: options.action,\n ...(options.payload ? { payload: options.payload } : {}),\n };\n frameWindow.postMessage(message, options.targetOrigin);\n}\n\nexport function useOeMeeting(options: UseOeMeetingOptions) {\n const iframeRef = useRef<HTMLIFrameElement | null>(null);\n const callbacksRef = useRef(options);\n\n useEffect(() => {\n callbacksRef.current = options;\n }, [options]);\n\n useEffect(() => {\n const onMessage = (event: MessageEvent<unknown>) => {\n const frameWindow = iframeRef.current?.contentWindow;\n if (\n !frameWindow ||\n event.source !== frameWindow ||\n event.origin !== options.targetOrigin\n ) {\n return;\n }\n const embedEvent = parseEmbedEvent(event.data);\n if (!embedEvent || embedEvent.instanceId !== options.instanceId) return;\n\n const callbacks = callbacksRef.current;\n callbacks.onEvent?.(embedEvent);\n switch (embedEvent.type) {\n case EmbedEventType.READY:\n callbacks.onReady?.(embedEvent);\n break;\n case EmbedEventType.JOINED:\n callbacks.onJoined?.(embedEvent);\n break;\n case EmbedEventType.LEFT:\n callbacks.onLeft?.(embedEvent);\n break;\n case EmbedEventType.PARTICIPANT_JOINED:\n callbacks.onParticipantJoined?.(embedEvent);\n break;\n case EmbedEventType.PARTICIPANT_LEFT:\n callbacks.onParticipantLeft?.(embedEvent);\n break;\n case EmbedEventType.ENDED:\n callbacks.onEnded?.(embedEvent);\n break;\n case EmbedEventType.ERROR:\n callbacks.onError?.(embedEvent);\n break;\n case EmbedEventType.STAGE_CHANGED:\n callbacks.onStageChanged?.(embedEvent);\n break;\n case EmbedEventType.TRANSCRIPT_STARTED:\n callbacks.onTranscriptStarted?.(embedEvent);\n break;\n case EmbedEventType.RECORDING_STARTED:\n callbacks.onRecordingStarted?.(embedEvent);\n break;\n }\n };\n\n window.addEventListener('message', onMessage);\n return () => window.removeEventListener('message', onMessage);\n }, [options.instanceId, options.targetOrigin]);\n\n const send = useCallback(\n (action: EmbedCommandAction, payload?: OpenStageInput) =>\n postCommand({\n iframe: iframeRef.current,\n instanceId: options.instanceId,\n action,\n targetOrigin: options.targetOrigin,\n payload,\n }),\n [options.instanceId, options.targetOrigin],\n );\n const leave = useCallback(() => send(EmbedCommandAction.LEAVE), [send]);\n const toggleMic = useCallback(\n () => send(EmbedCommandAction.TOGGLE_MIC),\n [send],\n );\n const toggleCamera = useCallback(\n () => send(EmbedCommandAction.TOGGLE_CAMERA),\n [send],\n );\n const openStage = useCallback(\n (input: OpenStageInput) => {\n const normalized = normalizeOpenStageInput(input);\n if (!normalized) return;\n send(EmbedCommandAction.OPEN_STAGE, normalized);\n },\n [send],\n );\n const closeStage = useCallback(\n () => send(EmbedCommandAction.CLOSE_STAGE),\n [send],\n );\n const getIframe = useCallback(() => iframeRef.current, []);\n const handle = useMemo<OeMeetingHandle>(\n () => ({\n leave,\n toggleMic,\n toggleCamera,\n openStage,\n closeStage,\n getIframe,\n }),\n [closeStage, getIframe, leave, openStage, toggleCamera, toggleMic],\n );\n\n useImperativeHandle(options.handleRef, () => handle, [handle]);\n return { iframeRef, handle };\n}\n","import { isStageToolId, MAX_STAGE_URL_LENGTH } from './embed-protocol';\nimport type { OpenStageInput } from './types';\n\nexport function isValidStageUrl(url: string): boolean {\n if (url.length === 0 || url.length > MAX_STAGE_URL_LENGTH) return false;\n try {\n const parsed = new URL(url);\n if (parsed.protocol === 'https:') return true;\n if (parsed.protocol === 'http:') {\n return (\n parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1'\n );\n }\n return false;\n } catch {\n return false;\n }\n}\n\nexport function normalizeOpenStageInput(\n input: OpenStageInput,\n): OpenStageInput | null {\n const url = input.url.trim();\n if (!isValidStageUrl(url)) return null;\n const title = input.title?.trim();\n return {\n url,\n ...(title ? { title: title.slice(0, 200) } : {}),\n ...(isStageToolId(input.tool) ? { tool: input.tool } : {}),\n };\n}\n","import {\n EMBED_MESSAGE_SOURCE,\n EMBED_PROTOCOL_VERSION,\n EmbedCaptureOutcome,\n EmbedEventType,\n} from './embed-protocol';\nimport type { EmbedEvent } from './types';\n\nconst EVENT_TYPES = new Set<string>(Object.values(EmbedEventType));\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction isOptionalString(value: unknown): boolean {\n return value === undefined || typeof value === 'string';\n}\n\nfunction isNullableString(value: unknown): boolean {\n return value === null || typeof value === 'string';\n}\n\nconst CAPTURE_OUTCOMES = new Set<string>(Object.values(EmbedCaptureOutcome));\n\nfunction isCaptureOutcome(value: unknown): boolean {\n return typeof value === 'string' && CAPTURE_OUTCOMES.has(value);\n}\n\nfunction hasValidPayload(\n type: EmbedEventType,\n payload: Record<string, unknown>,\n): boolean {\n switch (type) {\n case EmbedEventType.READY:\n return typeof payload.roomSlug === 'string';\n case EmbedEventType.JOINED:\n return (\n typeof payload.roomSlug === 'string' &&\n typeof payload.identity === 'string' &&\n isOptionalString(payload.name) &&\n typeof payload.joinedAt === 'string'\n );\n case EmbedEventType.LEFT:\n return (\n typeof payload.roomSlug === 'string' &&\n typeof payload.identity === 'string' &&\n typeof payload.leftAt === 'string' &&\n (payload.durationMs === undefined ||\n typeof payload.durationMs === 'number')\n );\n case EmbedEventType.PARTICIPANT_JOINED:\n return (\n typeof payload.identity === 'string' &&\n isOptionalString(payload.name) &&\n typeof payload.participantCount === 'number'\n );\n case EmbedEventType.PARTICIPANT_LEFT:\n return (\n typeof payload.identity === 'string' &&\n typeof payload.participantCount === 'number'\n );\n case EmbedEventType.ENDED:\n return (\n typeof payload.roomSlug === 'string' &&\n typeof payload.endedAt === 'string'\n );\n case EmbedEventType.ERROR:\n return (\n typeof payload.code === 'string' &&\n typeof payload.message === 'string'\n );\n case EmbedEventType.STAGE_CHANGED:\n return (\n (payload.url === null || typeof payload.url === 'string') &&\n isOptionalString(payload.title) &&\n isOptionalString(payload.tool) &&\n isOptionalString(payload.openedBy)\n );\n case EmbedEventType.TRANSCRIPT_STARTED:\n return (\n typeof payload.roomSlug === 'string' &&\n isCaptureOutcome(payload.outcome) &&\n isNullableString(payload.transcriptId) &&\n isNullableString(payload.status)\n );\n case EmbedEventType.RECORDING_STARTED:\n return (\n typeof payload.roomSlug === 'string' &&\n isCaptureOutcome(payload.outcome) &&\n isNullableString(payload.recordingId) &&\n isNullableString(payload.status)\n );\n }\n}\n\nexport function parseEmbedEvent(data: unknown): EmbedEvent | null {\n if (!isRecord(data)) return null;\n if (\n data.source !== EMBED_MESSAGE_SOURCE ||\n data.version !== EMBED_PROTOCOL_VERSION ||\n typeof data.instanceId !== 'string' ||\n data.instanceId.length === 0 ||\n typeof data.type !== 'string' ||\n !EVENT_TYPES.has(data.type) ||\n !isRecord(data.payload)\n ) {\n return null;\n }\n\n const type = data.type as EmbedEventType;\n if (!hasValidPayload(type, data.payload)) return null;\n return data as unknown as EmbedEvent;\n}\n"],"mappings":"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","names":["useMemo","EmbedEventType","EmbedCommandType","EmbedCommandAction","EmbedErrorCode","EmbedQueryParam","EmbedCaptureOutcome","OeMeeting","useMemo"]}
@@ -8,12 +8,14 @@ OE Meeting cần cấp cho OE.School một application credential gồm:
8
8
  - `clientSecret`.
9
9
  - Scope `rooms:create` để tạo phòng.
10
10
  - Scope `rooms:retrieve` để đọc phòng và cấp join token.
11
+ - Scope `transcripts:retrieve` để đọc transcript sau buổi học.
12
+ - Scope `recordings:retrieve` để tải bản ghi sau buổi học.
11
13
  - Tùy nhu cầu quản trị: `rooms:update`, `rooms:delete`.
12
14
 
13
15
  Cấu hình ở backend OE.School, ví dụ:
14
16
 
15
17
  ```dotenv
16
- OE_MEETING_API_URL=https://meet.oceanedu.org
18
+ OE_MEETING_API_URL=https://meet.ocean.edu.vn
17
19
  OE_MEETING_CLIENT_ID=oe-school
18
20
  OE_MEETING_CLIENT_SECRET=<secret-from-secret-manager>
19
21
  ```
@@ -27,7 +29,7 @@ vào repository.
27
29
  Base URL production:
28
30
 
29
31
  ```text
30
- https://meet.oceanedu.org/external-api/v1.0
32
+ https://meet.ocean.edu.vn/external-api/v1.0
31
33
  ```
32
34
 
33
35
  Mọi request server-to-server phải gửi:
@@ -123,7 +125,67 @@ Nên dùng identity có namespace, ví dụ `oe-school:user:<userId>`, để tr
123
125
  identity giữa các hệ thống tích hợp. Không dùng token phiên đăng nhập OE.School
124
126
  làm identity.
125
127
 
126
- ### 2.3. Các endpoint quản trị khác
128
+ ### 2.3. Lấy transcript của buổi họp
129
+
130
+ ```http
131
+ GET /external-api/v1.0/rooms/:idOrSlug/transcript
132
+ ```
133
+
134
+ Scope: `transcripts:retrieve`. `:idOrSlug` là room id hoặc slug đã tạo ở 2.1 —
135
+ cùng định danh mà OE.School đã lưu vào bản ghi buổi học.
136
+
137
+ Response mẫu:
138
+
139
+ ```json
140
+ {
141
+ "id": "8f1c0f2a-2c9e-4f2f-9a3b-1e6d5b0c7a11",
142
+ "roomId": "c4ac4f15-8417-4e19-a3dd-39de5697c19f",
143
+ "status": "completed",
144
+ "startedBy": null,
145
+ "startedByName": "Nguyen Van A",
146
+ "createdAt": "2026-08-17T02:00:12.000Z",
147
+ "endedAt": "2026-08-17T02:48:31.000Z",
148
+ "segments": [
149
+ {
150
+ "id": "0b0f9d54-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
151
+ "participantIdentity": "oe-school:user:12345",
152
+ "participantName": "Nguyen Van A",
153
+ "text": "Chào cả lớp, hôm nay chúng ta học bài 3.",
154
+ "language": "vi",
155
+ "spokenAt": "2026-08-17T02:00:45.000Z"
156
+ }
157
+ ]
158
+ }
159
+ ```
160
+
161
+ | Field | Mô tả |
162
+ |---|---|
163
+ | `status` | `active` khi buổi học đang diễn ra, `paused` khi tạm dừng, `completed` sau khi phòng kết thúc. |
164
+ | `segments` | Các câu nói đã chốt, sắp xếp tăng dần theo `spokenAt`. |
165
+ | `startedBy` | `null` với phiên do embed tự bật (người tham gia là khách của OE.School, không có tài khoản OE Meeting). |
166
+
167
+ Trả `404` khi phòng chưa từng có transcript nào. Transcript là tài sản cấp
168
+ phòng: nếu OE.School dùng lại một room cho nhiều buổi, endpoint trả phiên gần
169
+ nhất và các segment của phiên đó. Dùng một room cho mỗi buổi học (theo quy ước
170
+ slug ở 2.1) để tránh nhập nhằng.
171
+
172
+ Gọi endpoint này sau khi nhận sự kiện kết thúc buổi học. Agent transcriber chốt
173
+ phiên (`status: "completed"`) khi phòng LiveKit đóng, nên gọi quá sớm sẽ nhận
174
+ được transcript còn `active` và thiếu các câu cuối.
175
+
176
+ ### 2.4. Lấy bản ghi của buổi họp
177
+
178
+ ```http
179
+ GET /external-api/v1.0/rooms/:idOrSlug/recordings
180
+ ```
181
+
182
+ Scope: `recordings:retrieve`. Response trả `results` theo thứ tự mới nhất trước.
183
+ Bản ghi có `status: "saved"` và chưa hết hạn sẽ có `downloadUrl` tạm thời;
184
+ recording đang xử lý hoặc đã hết hạn trả `downloadUrl: null`. Hệ thống ngừng
185
+ cho tải sau `RECORDING_EXPIRATION_DAYS`; cấu hình lifecycle S3/MinIO cùng số
186
+ ngày trên prefix `RECORDING_OUTPUT_FOLDER` để file được xóa khỏi storage.
187
+
188
+ ### 2.5. Các endpoint quản trị khác
127
189
 
128
190
  | Method | Endpoint | Scope |
129
191
  |---|---|---|
@@ -151,6 +213,26 @@ interface JoinTokenResponse {
151
213
  token: string;
152
214
  }
153
215
 
216
+ interface TranscriptSegment {
217
+ id: string;
218
+ participantIdentity: string | null;
219
+ participantName: string;
220
+ text: string;
221
+ language: string | null;
222
+ spokenAt: string;
223
+ }
224
+
225
+ interface MeetingTranscript {
226
+ id: string;
227
+ roomId: string;
228
+ status: 'active' | 'paused' | 'completed';
229
+ startedBy: string | null;
230
+ startedByName: string | null;
231
+ createdAt: string;
232
+ endedAt: string | null;
233
+ segments: TranscriptSegment[];
234
+ }
235
+
154
236
  interface MeetingCredentials {
155
237
  apiUrl: string;
156
238
  clientId: string;
@@ -206,6 +288,13 @@ export class OeMeetingApi {
206
288
  }),
207
289
  });
208
290
  }
291
+
292
+ getLessonTranscript(roomSlug: string): Promise<MeetingTranscript> {
293
+ return this.request(
294
+ `/external-api/v1.0/rooms/${encodeURIComponent(roomSlug)}/transcript`,
295
+ { method: 'GET' },
296
+ );
297
+ }
209
298
  }
210
299
  ```
211
300
 
@@ -93,7 +93,7 @@ export function OnlineLesson({ lessonId }: { lessonId: string }) {
93
93
  <OeMeeting
94
94
  roomSlug={access.roomSlug}
95
95
  joinToken={access.joinToken}
96
- origin="https://meet.oceanedu.org"
96
+ origin="https://meet.ocean.edu.vn"
97
97
  width="100%"
98
98
  height="calc(100vh - 64px)"
99
99
  hideLeave
@@ -106,7 +106,7 @@ export function OnlineLesson({ lessonId }: { lessonId: string }) {
106
106
  }
107
107
  ```
108
108
 
109
- `origin` có giá trị mặc định là `https://meet.oceanedu.org`, nên có thể bỏ prop
109
+ `origin` có giá trị mặc định là `https://meet.ocean.edu.vn`, nên có thể bỏ prop
110
110
  này ở production. Khi chạy local, truyền URL frontend OE Meeting local, ví dụ
111
111
  `http://localhost:5173`.
112
112
 
@@ -116,9 +116,11 @@ này ở production. Khi chạy local, truyền URL frontend OE Meeting local, v
116
116
  |---|---|---|---|
117
117
  | `roomSlug` | `string` | Bắt buộc | Slug phòng do backend trả về. |
118
118
  | `joinToken` | `string` | Bắt buộc | Token cấp riêng cho user hiện tại. |
119
- | `origin` | `string` | `https://meet.oceanedu.org` | Origin của OE Meeting. |
119
+ | `origin` | `string` | `https://meet.ocean.edu.vn` | Origin của OE Meeting. |
120
120
  | `autoJoin` | `boolean` | `true` | Phiên bản hiện tại chỉ hỗ trợ `true`. |
121
121
  | `hideLeave` | `boolean` | `false` | Ẩn nút rời phòng trong iframe. |
122
+ | `autoTranscribe` | `boolean` | `false` | Tự bật transcript dùng chung khi embed kết nối. |
123
+ | `autoRecord` | `boolean` | `false` | Tự ghi toàn bộ phòng khi embed kết nối. |
122
124
  | `userName` | `string` | — | Thông tin tùy chọn truyền vào embed URL. |
123
125
  | `externalUserId` | `string` | — | External identity tùy chọn. |
124
126
  | `lang` | `string` | — | Ngôn ngữ giao diện. |
@@ -138,6 +140,8 @@ này ở production. Khi chạy local, truyền URL frontend OE Meeting local, v
138
140
  | `onParticipantLeft` | callback | — | Một participant khác rời phòng. |
139
141
  | `onEnded` | callback | — | Phòng kết thúc. |
140
142
  | `onError` | callback | — | Lỗi typed từ embed. |
143
+ | `onTranscriptStarted` | callback | — | Kết quả của `autoTranscribe`. |
144
+ | `onRecordingStarted` | callback | — | Kết quả của `autoRecord`. |
141
145
  | `onEvent` | callback | — | Nhận tất cả event protocol. |
142
146
 
143
147
  Không đặt `targetOrigin="*"`. SDK và iframe đều kiểm tra origin, source và
@@ -166,10 +170,67 @@ interface EmbedEventEnvelope<TPayload> {
166
170
  | `PARTICIPANT_LEFT` | `identity`, `participantCount` | Đã emit |
167
171
  | `ERROR` | `code`, `message` | Đã emit |
168
172
  | `ENDED` | `roomSlug`, `endedAt` | Đã khai báo, chưa có nguồn emit |
173
+ | `STAGE_CHANGED` | `url` (`string` \| `null`), `title?`, `tool?`, `openedBy?` | Đã emit |
174
+ | `TRANSCRIPT_STARTED` | `roomSlug`, `outcome`, `transcriptId` (`string` \| `null`), `status` (`string` \| `null`) | Đã emit khi bật `autoTranscribe` |
175
+ | `RECORDING_STARTED` | `roomSlug`, `outcome`, `recordingId` (`string` \| `null`), `status` (`string` \| `null`) | Đã emit khi bật `autoRecord` |
169
176
 
170
177
  Có thể dùng `onEvent` để gửi analytics tập trung, nhưng không log join token,
171
178
  iframe URL đầy đủ hoặc dữ liệu nhạy cảm.
172
179
 
180
+ ### 4.1. Tự động bật transcript
181
+
182
+ ```tsx
183
+ <OeMeeting
184
+ roomSlug={access.roomSlug}
185
+ joinToken={access.joinToken}
186
+ autoTranscribe
187
+ onTranscriptStarted={(event) => {
188
+ console.info('transcript', event.payload.outcome, event.payload.transcriptId);
189
+ }}
190
+ />
191
+ ```
192
+
193
+ Transcript là tài sản cấp phòng, không phải cấp participant: người đầu tiên kết
194
+ nối bật nó lên, mọi người vào sau nhận `outcome: "already_running"`, và nó vẫn
195
+ chạy khi người bật rời phòng sớm.
196
+
197
+ | `outcome` | Ý nghĩa |
198
+ |---|---|
199
+ | `started` | Lần kết nối này đã bật lên. |
200
+ | `already_running` | Đã chạy sẵn, trạng thái bình thường của mọi người vào sau. |
201
+ | `disabled` | Deployment OE Meeting tắt transcript. |
202
+ | `failed` | Không bật được; kèm theo một event `ERROR`. |
203
+
204
+ Request bật transcript được xác thực bằng chính join token của participant, nên
205
+ OE.School không cần thêm credential nào ở phía trình duyệt. Đổi lại, transcript
206
+ chỉ bắt đầu khi có người thực sự vào phòng: một buổi học không ai vào sẽ không
207
+ có transcript.
208
+
209
+ ### 4.2. Tự động ghi hình
210
+
211
+ ```tsx
212
+ <OeMeeting
213
+ roomSlug={access.roomSlug}
214
+ joinToken={access.joinToken}
215
+ autoRecord
216
+ onRecordingStarted={(event) => {
217
+ console.info('recording', event.payload.outcome, event.payload.recordingId);
218
+ }}
219
+ />
220
+ ```
221
+
222
+ Recording cũng là tài sản cấp phòng: người đầu tiên kết nối bật egress,
223
+ mọi người vào sau nhận `outcome: "already_running"`. Local cần MinIO +
224
+ LiveKit Egress (`docker compose --profile s3 up -d`).
225
+
226
+ Lấy danh sách và URL tải sau buổi học bằng
227
+ `GET /external-api/v1.0/rooms/:idOrSlug/recordings` từ backend OE.School —
228
+ xem `BACKEND-INTEGRATION.md` mục 2.4. Đừng gọi external API từ trình duyệt:
229
+ nó cần `X-Client-Secret`.
230
+
231
+ Lấy transcript sau buổi học bằng `GET /external-api/v1.0/rooms/:idOrSlug/transcript`
232
+ từ backend OE.School — xem `BACKEND-INTEGRATION.md` mục 2.3.
233
+
173
234
  ## 5. Điều khiển phòng từ trang cha
174
235
 
175
236
  ```tsx
@@ -213,6 +274,8 @@ export function MeetingWithControls(props: {
213
274
  - `leave()`.
214
275
  - `toggleMic()`.
215
276
  - `toggleCamera()`.
277
+ - `openStage({ url, title?, tool? })` — ignore nếu `attributes.stage=follow`.
278
+ - `closeStage()` — opener, `stage=open`, hoặc admin room.
216
279
  - `getIframe()`.
217
280
 
218
281
  ## 6. Error codes
@@ -226,14 +289,18 @@ export function MeetingWithControls(props: {
226
289
  | `ROOM_NOT_FOUND` | Phòng không tồn tại. | Tạo/đồng bộ lại phòng của lesson. | Đã khai báo, chưa phân loại riêng |
227
290
  | `ROOM_ACCESS_DENIED` | User không có quyền. | Không retry tự động; hiển thị thông báo phù hợp. | Đã khai báo, chưa phân loại riêng |
228
291
  | `PROTOCOL_MISMATCH` | SDK và embed khác version. | Đồng bộ phiên bản SDK/OE Meeting. | Đã khai báo, chưa emit |
292
+ | `STAGE_ORIGIN_DENIED` | Origin URL Stage không nằm allowlist. | Dùng origin đã khai báo (`hostOrigin` / `VITE_STAGE_ALLOWED_ORIGINS`). | Đã emit |
293
+ | `STAGE_URL_INVALID` | URL Stage thiếu hoặc không hợp lệ. | Chỉ `https` (hoặc `http` localhost). | Đã emit |
294
+ | `TRANSCRIPT_START_FAILED` | `autoTranscribe` không bật được transcript. | Không retry từ trang cha; kiểm tra agent transcriber. | Đã emit |
295
+ | `RECORDING_START_FAILED` | `autoRecord` không bật được recording. | Không retry từ trang cha; kiểm tra LiveKit Egress và S3. | Đã emit |
229
296
 
230
297
  ## 7. Local development
231
298
 
232
- Giả sử OE Meeting frontend chạy tại `http://localhost:5173`:
299
+ Frontend OE Meeting mặc định chạy cổng `3000`.
233
300
 
234
301
  ```tsx
235
302
  <OeMeeting
236
- origin="http://localhost:5173"
303
+ origin="http://localhost:3000"
237
304
  roomSlug={roomSlug}
238
305
  joinToken={joinToken}
239
306
  />
@@ -266,7 +333,7 @@ về đúng origin đó.
266
333
  ### Frontend OE.School
267
334
 
268
335
  - [ ] Chỉ render `OeMeeting` sau khi nhận `roomSlug` và `joinToken`.
269
- - [ ] Production dùng origin `https://meet.oceanedu.org`.
336
+ - [ ] Production dùng origin `https://meet.ocean.edu.vn`.
270
337
  - [ ] Có trạng thái loading, forbidden, token expired và connection failed.
271
338
  - [ ] Không dùng `targetOrigin="*"`.
272
339
  - [ ] Camera/microphone hoạt động trong iframe.
@@ -289,7 +356,7 @@ về đúng origin đó.
289
356
  - OE Meeting chưa đồng bộ roster vào database; OE.School chịu trách nhiệm duyệt
290
357
  quyền trước mỗi lần cấp token.
291
358
  - Chưa có API thu hồi token đã cấp hoặc kick participant theo lesson attendance.
292
- - Protocol v1 chưa có recording events và token handshake qua `postMessage`.
359
+ - Protocol v1 chưa có token handshake qua `postMessage`.
293
360
  - `autoJoin={false}` chưa có prejoin flow; phiên bản hiện tại yêu cầu auto join.
294
361
  - Các lỗi token/access chưa được phân loại riêng; frontend có thể nhận
295
362
  `CONNECTION_FAILED` cho các lỗi kết nối liên quan token.
package/docs/RELEASE.md CHANGED
@@ -1,36 +1,39 @@
1
1
  # Release npm — `@tinyweb_dev/oe-meeting-react`
2
2
 
3
3
  Workflow [`release_npm.yml`](../../../../.github/workflows/release_npm.yml) build,
4
- typecheck, test, kiểm tra tarball và publish React SDK lên npm.
4
+ typecheck, test, kiểm tra tarball và publish React SDK lên npm bằng
5
+ **npm Trusted Publishing (OIDC)** — không dùng long-lived `NPM_TOKEN`.
5
6
 
6
7
  ## Chuẩn bị một lần
7
8
 
8
9
  ### 1. Kiểm tra npm organization
9
10
 
10
- Tài khoản phát hành phải có quyền publish package public trong scope
11
- `@tinyweb_dev`.
11
+ Tài khoản maintainer phải có quyền cấu hình Trusted Publisher cho package
12
+ public trong scope `@tinyweb_dev`.
12
13
 
13
- ### 2. Tạo npm access token
14
+ ### 2. Cấu hình Trusted Publisher trên npmjs.com
14
15
 
15
- Tạo automation/granular access token trên npm với quyền read/write package
16
- `@tinyweb_dev/oe-meeting-react`.
17
-
18
- ### 3. Thêm GitHub Actions secret
19
-
20
- Trong repository `tinyweb-01/oe-meeting`, mở:
16
+ Trên npmjs.com, mở package (hoặc org) `@tinyweb_dev/oe-meeting-react` →
17
+ **Trusted Publisher** / **Publishing access**:
21
18
 
22
19
  ```text
23
- Settings → Secrets and variables → Actions → New repository secret
20
+ Organization or user: tinyweb-01
21
+ Repository: oe-meeting
22
+ Workflow filename: release_npm.yml
23
+ Allowed actions: npm publish
24
24
  ```
25
25
 
26
- Tạo secret:
26
+ Không cần GitHub secret `NPM_TOKEN`. Workflow dùng OIDC (`id-token: write`)
27
+ và npm CLI >= 11.5.1 trên Node >= 22.14.
27
28
 
28
- ```text
29
- Name: NPM_TOKEN
30
- Value: <npm-access-token>
31
- ```
29
+ Granular access token (GAT) bypass-2FA không còn phù hợp cho CI publish
30
+ ([npm v12 / GAT deprecation](https://github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation/)).
31
+
32
+ ### 3. Publish lần đầu (package chưa tồn tại)
32
33
 
33
- Không ghi token vào source, workflow, log hoặc file `.npmrc` trong repository.
34
+ Nếu package chưa có trên registry, tạo Trusted Publisher ở cấp org
35
+ `@tinyweb_dev` trỏ đúng GitHub repo/workflow ở trên, rồi chạy release.
36
+ Không commit token vào source, workflow, log hoặc file `.npmrc`.
34
37
 
35
38
  ## Kiểm thử pipeline không publish
36
39
 
@@ -43,7 +46,7 @@ dry_run = true
43
46
  Workflow sẽ chạy đầy đủ build, typecheck, test, `npm pack` và
44
47
  `npm publish --dry-run`; không upload package.
45
48
 
46
- Có thể kiểm tra tương đương ở local:
49
+ Có thể kiểm tra tương đương ở local (local publish vẫn cần 2FA tương tác):
47
50
 
48
51
  ```bash
49
52
  cd src/sdk
@@ -105,12 +108,20 @@ bản mới.
105
108
  một commit/ref đã review và package version chưa tồn tại trên npm. Luồng tag vẫn
106
109
  là cách phát hành chuẩn vì tạo dấu mốc release rõ ràng trong Git.
107
110
 
111
+ Local publish (maintainer + 2FA):
112
+
113
+ ```bash
114
+ cd src/sdk/react
115
+ npm publish --access public
116
+ ```
117
+
108
118
  ## Troubleshooting
109
119
 
110
120
  | Lỗi | Nguyên nhân thường gặp | Cách xử lý |
111
121
  |---|---|---|
112
- | `ENEEDAUTH` / `E401` | Thiếu, hết hạn hoặc sai `NPM_TOKEN` | Tạo token mới và cập nhật GitHub secret. |
113
- | `E403` | Token không có quyền scope/package hoặc npm yêu cầu 2FA policy khác | Kiểm tra quyền granular token và organization. |
122
+ | `E404` trên `PUT .../oe-meeting-react` | Token/GAT bị từ chối (npm trả 404 thay vì 401/403), hoặc chưa cấu hình Trusted Publisher | Cấu hình OIDC Trusted Publisher; không dùng `NPM_TOKEN` bypass-2FA. |
123
+ | `ENEEDAUTH` / `E401` | OIDC chưa được npm chấp nhận, hoặc local thiếu login | Kiểm tra Trusted Publisher khớp org/repo/workflow; local thì `npm login` + 2FA. |
124
+ | `E403` | Publisher không có quyền scope/package | Kiểm tra quyền org `@tinyweb_dev` và Trusted Publisher. |
114
125
  | Tag không khớp version | Tag và `package.json` khác nhau | Tăng version, commit rồi tạo tag đúng convention. |
115
126
  | Version đã tồn tại | Npm không cho overwrite | Tăng patch/minor/major version. |
116
127
  | Build/typecheck/test fail | Source chưa đạt release gate | Fix lỗi; không bỏ qua gate publish. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinyweb_dev/oe-meeting-react",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "React SDK to embed OE Meeting rooms via iframe and postMessage",
5
5
  "publishConfig": {
6
6
  "access": "public"