@shadowob/sdk
Advanced tools
| // src/server-app.ts | ||
| import { | ||
| BUDDY_INBOX_DELIVERY_PERMISSION | ||
| } from "@shadowob/shared"; | ||
| var SHADOW_SERVER_APP_PROTOCOL = "shadow.app/1"; | ||
| var SHADOW_SERVER_APP_COMMAND_COMPLETED_EVENT = "server_app.command.completed"; | ||
| var SHADOW_SERVER_APP_COMMAND_FAILED_EVENT = "server_app.command.failed"; | ||
| var SHADOW_SERVER_APP_COMMAND_EVENTS = [ | ||
| SHADOW_SERVER_APP_COMMAND_COMPLETED_EVENT, | ||
| SHADOW_SERVER_APP_COMMAND_FAILED_EVENT | ||
| ]; | ||
| var ShadowServerAppHttpError = class extends Error { | ||
| status; | ||
| payload; | ||
| constructor(status, message, payload) { | ||
| super(message); | ||
| this.name = "ShadowServerAppHttpError"; | ||
| this.status = status; | ||
| this.payload = payload; | ||
| } | ||
| }; | ||
| function isProtocolRecord(value) { | ||
| return !!value && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| function optionalProtocolString(value) { | ||
| return typeof value === "string" && value ? value : void 0; | ||
| } | ||
| function protocolPathSegment(value) { | ||
| return encodeURIComponent(value); | ||
| } | ||
| function shadowServerAppInboxTaskEndpoint(serverIdOrSlug, target) { | ||
| if ("channelId" in target && target.channelId) { | ||
| return `/api/channels/${protocolPathSegment(target.channelId)}/inbox/tasks`; | ||
| } | ||
| if ("agentId" in target && target.agentId) { | ||
| return `/api/servers/${protocolPathSegment(serverIdOrSlug)}/inboxes/${protocolPathSegment( | ||
| target.agentId | ||
| )}/tasks`; | ||
| } | ||
| throw new Error("Missing Inbox task target"); | ||
| } | ||
| function buildShadowServerAppInboxTaskRequest(input) { | ||
| const appId = input.app.id ?? input.app.appId ?? input.app.appKey; | ||
| const serverAppData = isProtocolRecord(input.task.data?.serverApp) ? input.task.data.serverApp : {}; | ||
| return { | ||
| endpoint: shadowServerAppInboxTaskEndpoint(input.serverIdOrSlug, input.target), | ||
| body: { | ||
| title: input.task.title, | ||
| body: input.task.body, | ||
| priority: input.task.priority, | ||
| tags: input.task.tags, | ||
| idempotencyKey: input.task.idempotencyKey, | ||
| requirements: input.task.requirements, | ||
| outputContract: input.task.outputContract, | ||
| privacy: input.task.privacy, | ||
| app: { | ||
| id: appId, | ||
| appId, | ||
| appKey: input.app.appKey, | ||
| name: input.app.name ?? input.app.label ?? input.app.appKey, | ||
| label: input.app.label ?? input.app.name ?? input.app.appKey, | ||
| ...input.app.iconUrl ? { iconUrl: input.app.iconUrl } : {} | ||
| }, | ||
| source: { | ||
| kind: "server_app", | ||
| id: appId, | ||
| appId, | ||
| appKey: input.app.appKey, | ||
| ...input.app.name ? { appName: input.app.name } : {}, | ||
| ...input.app.iconUrl ? { iconUrl: input.app.iconUrl } : {}, | ||
| ...input.app.serverId ? { serverId: input.app.serverId } : {}, | ||
| ...input.commandName ? { command: input.commandName } : {}, | ||
| label: input.app.label ?? input.app.name ?? input.app.appKey, | ||
| ...input.task.resource ? { resource: input.task.resource } : {} | ||
| }, | ||
| data: { | ||
| ...input.task.data ?? {}, | ||
| serverApp: { | ||
| ...serverAppData, | ||
| appKey: input.app.appKey, | ||
| name: input.app.name ?? input.app.label ?? input.app.appKey, | ||
| label: input.app.label ?? input.app.name ?? input.app.appKey, | ||
| ...input.app.iconUrl ? { iconUrl: input.app.iconUrl } : {}, | ||
| ...input.commandName ? { command: input.commandName } : {} | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| function getShadowServerAppTaskCardId(message) { | ||
| const metadata = isProtocolRecord(message) ? message.metadata : null; | ||
| const cards = isProtocolRecord(metadata) && Array.isArray(metadata.cards) ? metadata.cards : []; | ||
| for (const item of cards) { | ||
| if (!isProtocolRecord(item)) continue; | ||
| if (item.kind === "task" && typeof item.id === "string" && item.id) return item.id; | ||
| } | ||
| return null; | ||
| } | ||
| function buildShadowServerAppInboxDelivery(input) { | ||
| const message = isProtocolRecord(input.message) ? input.message : {}; | ||
| return { | ||
| ..."agentId" in input.target && input.target.agentId ? { agentId: input.target.agentId } : {}, | ||
| channelId: optionalProtocolString(message.channelId), | ||
| messageId: optionalProtocolString(message.id), | ||
| cardId: getShadowServerAppTaskCardId(message), | ||
| idempotencyKey: input.idempotencyKey | ||
| }; | ||
| } | ||
| function shadowFromPayload(payload) { | ||
| if (payload.protocol === SHADOW_SERVER_APP_PROTOCOL) { | ||
| return payload; | ||
| } | ||
| const shadow = isProtocolRecord(payload.shadow) ? payload.shadow : null; | ||
| if (shadow?.protocol === SHADOW_SERVER_APP_PROTOCOL) { | ||
| return shadow; | ||
| } | ||
| return null; | ||
| } | ||
| function mergeShadowResult(value, shadow) { | ||
| if (!shadow) return value; | ||
| const existing = shadowFromPayload(value); | ||
| if (!existing) return { ...value, shadow }; | ||
| return { | ||
| ...value, | ||
| shadow: { | ||
| protocol: SHADOW_SERVER_APP_PROTOCOL, | ||
| outbox: { | ||
| ...existing.outbox ?? {}, | ||
| ...shadow.outbox ?? {} | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| function getShadowServerAppInboxDeliveries(payload) { | ||
| if (!isProtocolRecord(payload)) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| return shadow?.outbox?.deliveries ?? []; | ||
| } | ||
| function getShadowServerAppInboxErrors(payload) { | ||
| if (!isProtocolRecord(payload)) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| return shadow?.outbox?.errors ?? []; | ||
| } | ||
| function getShadowServerAppPendingInboxTasks(payload, depth = 0) { | ||
| if (!isProtocolRecord(payload) || depth > 4) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| const tasks = shadow?.outbox?.inboxTasks ?? []; | ||
| return "result" in payload && payload.result !== void 0 ? [...tasks, ...getShadowServerAppPendingInboxTasks(payload.result, depth + 1)] : tasks; | ||
| } | ||
| function getShadowServerAppChannelMessageDeliveries(payload) { | ||
| if (!isProtocolRecord(payload)) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| return shadow?.outbox?.channelMessageDeliveries ?? []; | ||
| } | ||
| function getShadowServerAppChannelMessageErrors(payload) { | ||
| if (!isProtocolRecord(payload)) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| return shadow?.outbox?.channelMessageErrors ?? []; | ||
| } | ||
| function getShadowServerAppPendingChannelMessages(payload, depth = 0) { | ||
| if (!isProtocolRecord(payload) || depth > 4) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| const messages = shadow?.outbox?.channelMessages ?? []; | ||
| return "result" in payload && payload.result !== void 0 ? [...messages, ...getShadowServerAppPendingChannelMessages(payload.result, depth + 1)] : messages; | ||
| } | ||
| function hasShadowServerAppPendingOutbox(payload) { | ||
| return getShadowServerAppPendingInboxTasks(payload).length > 0 || getShadowServerAppPendingChannelMessages(payload).length > 0; | ||
| } | ||
| function isDomainResultWithEvents(payload) { | ||
| return Array.isArray(payload.events) && ("cursor" in payload || "result" in payload); | ||
| } | ||
| function isCommandPayloadEnvelope(payload) { | ||
| if (isDomainResultWithEvents(payload)) return false; | ||
| return payload.ok === true || payload.ok === false || shadowFromPayload(payload) !== null; | ||
| } | ||
| function unwrapShadowServerAppCommandPayload(payload) { | ||
| if (isProtocolRecord(payload) && payload.ok === false) { | ||
| throw new Error(typeof payload.error === "string" ? payload.error : "Command failed"); | ||
| } | ||
| if (isProtocolRecord(payload) && "result" in payload && payload.result !== void 0 && isCommandPayloadEnvelope(payload)) { | ||
| const nested = unwrapShadowServerAppCommandPayload(payload.result); | ||
| const shadow = shadowFromPayload(payload); | ||
| if (isProtocolRecord(nested)) return mergeShadowResult(nested, shadow); | ||
| return nested; | ||
| } | ||
| return payload; | ||
| } | ||
| async function readShadowServerAppResponsePayload(response) { | ||
| const text = await response.text().catch(() => ""); | ||
| if (!text.trim()) return null; | ||
| try { | ||
| return JSON.parse(text); | ||
| } catch { | ||
| if (!response.ok) return { ok: false, error: text }; | ||
| throw new ShadowServerAppHttpError(response.status, "Command returned invalid JSON", text); | ||
| } | ||
| } | ||
| function shadowServerAppResponseErrorMessage(status, payload, fallback = "Command failed") { | ||
| if (isProtocolRecord(payload) && typeof payload.error === "string" && payload.error) { | ||
| return payload.error; | ||
| } | ||
| if (typeof payload === "string" && payload.trim()) return payload; | ||
| return status ? `${fallback} (${status})` : fallback; | ||
| } | ||
| async function readShadowServerAppCommandResponse(response) { | ||
| const payload = await readShadowServerAppResponsePayload(response); | ||
| if (!response.ok || isProtocolRecord(payload) && payload.ok === false) { | ||
| throw new ShadowServerAppHttpError( | ||
| response.status, | ||
| shadowServerAppResponseErrorMessage(response.status, payload), | ||
| payload | ||
| ); | ||
| } | ||
| return unwrapShadowServerAppCommandPayload(payload); | ||
| } | ||
| var ShadowServerAppOutbox = class { | ||
| inboxTasks = []; | ||
| channelMessages = []; | ||
| enqueueInboxTask(task) { | ||
| this.inboxTasks.push(task); | ||
| return this; | ||
| } | ||
| enqueueInboxTasks(tasks) { | ||
| for (const task of tasks) this.enqueueInboxTask(task); | ||
| return this; | ||
| } | ||
| sendChannelMessage(message) { | ||
| this.channelMessages.push(message); | ||
| return this; | ||
| } | ||
| sendChannelMessages(messages) { | ||
| for (const message of messages) this.sendChannelMessage(message); | ||
| return this; | ||
| } | ||
| toShadow() { | ||
| return { | ||
| protocol: SHADOW_SERVER_APP_PROTOCOL, | ||
| outbox: { | ||
| ...this.inboxTasks.length > 0 ? { inboxTasks: [...this.inboxTasks] } : {}, | ||
| ...this.channelMessages.length > 0 ? { channelMessages: [...this.channelMessages] } : {} | ||
| } | ||
| }; | ||
| } | ||
| attachTo(result) { | ||
| return { ...result, shadow: this.toShadow() }; | ||
| } | ||
| }; | ||
| function trimTrailingSlash(value) { | ||
| return value.replace(/\/$/, ""); | ||
| } | ||
| function joinBasePath(baseUrl, path) { | ||
| const cleanBase = trimTrailingSlash(baseUrl); | ||
| const cleanPath = path.startsWith("/") ? path : `/${path}`; | ||
| return `${cleanBase}${cleanPath}`; | ||
| } | ||
| function urlOrigin(value) { | ||
| try { | ||
| return new URL(value).origin; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function rebasePublicAssetUrl(value, sourceOrigin, publicBaseUrl) { | ||
| if (!sourceOrigin) return value; | ||
| try { | ||
| const url = new URL(value); | ||
| if (url.origin !== sourceOrigin) return value; | ||
| return joinBasePath(publicBaseUrl, `${url.pathname}${url.search}${url.hash}`); | ||
| } catch { | ||
| return value; | ||
| } | ||
| } | ||
| function extractShadowServerAppBearerToken(value) { | ||
| if (!value) return null; | ||
| return value.toLowerCase().startsWith("bearer ") ? value.slice(7).trim() : null; | ||
| } | ||
| function decodeBase64UrlJson(value) { | ||
| try { | ||
| const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); | ||
| const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="); | ||
| const binary = globalThis.atob(padded); | ||
| const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); | ||
| return JSON.parse(new TextDecoder().decode(bytes)); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function decodeShadowServerAppLaunchTokenHint(token) { | ||
| if (!token) return null; | ||
| const parts = token.split("."); | ||
| if (parts.length !== 3 || parts[0] !== "sat_v1") return null; | ||
| const payload = decodeBase64UrlJson(parts[1]); | ||
| if (typeof payload?.serverId !== "string" || typeof payload.appKey !== "string") return null; | ||
| return { serverId: payload.serverId, appKey: payload.appKey }; | ||
| } | ||
| async function fetchShadowServerAppLaunchInboxes(options) { | ||
| const hint = decodeShadowServerAppLaunchTokenHint(options.launchToken); | ||
| if (!hint || !options.launchToken) return { inboxes: [] }; | ||
| const fetchFn = options.fetch ?? fetch; | ||
| const baseUrl = trimTrailingSlash(options.shadowApiBaseUrl ?? "http://localhost:3002"); | ||
| const response = await fetchFn( | ||
| `${baseUrl}/api/servers/${encodeURIComponent(hint.serverId)}/apps/${encodeURIComponent( | ||
| hint.appKey | ||
| )}/launch/inboxes`, | ||
| { headers: { Authorization: `Bearer ${options.launchToken}` } } | ||
| ); | ||
| if (!response.ok) { | ||
| const message = await response.text().catch(() => ""); | ||
| throw new Error(`Shadow launch inbox lookup failed (${response.status}): ${message}`); | ||
| } | ||
| return await response.json(); | ||
| } | ||
| async function deliverShadowServerAppLaunchOutbox(options) { | ||
| const hint = decodeShadowServerAppLaunchTokenHint(options.launchToken); | ||
| if (!hint || !options.launchToken) return options.result; | ||
| const fetchFn = options.fetch ?? fetch; | ||
| const baseUrl = trimTrailingSlash(options.shadowApiBaseUrl ?? "http://localhost:3002"); | ||
| const response = await fetchFn( | ||
| `${baseUrl}/api/servers/${encodeURIComponent(hint.serverId)}/apps/${encodeURIComponent( | ||
| hint.appKey | ||
| )}/launch/outbox`, | ||
| { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${options.launchToken}`, | ||
| "Content-Type": "application/json" | ||
| }, | ||
| body: JSON.stringify({ | ||
| commandName: options.commandName, | ||
| result: options.result | ||
| }) | ||
| } | ||
| ); | ||
| if (!response.ok) { | ||
| const payload = await readShadowServerAppResponsePayload(response); | ||
| throw new ShadowServerAppHttpError( | ||
| response.status, | ||
| shadowServerAppResponseErrorMessage( | ||
| response.status, | ||
| payload, | ||
| "Shadow launch outbox delivery failed" | ||
| ), | ||
| payload | ||
| ); | ||
| } | ||
| return readShadowServerAppResponsePayload(response); | ||
| } | ||
| function normalizeShadowServerAppCommandInput(value) { | ||
| if (value && typeof value === "object" && !Array.isArray(value) && "input" in value && Object.keys(value).every((key) => key === "input" || key === "channelId")) { | ||
| return value.input ?? {}; | ||
| } | ||
| return value; | ||
| } | ||
| function createShadowServerAppManifest(manifest, options = {}) { | ||
| const publicBaseUrl = trimTrailingSlash( | ||
| options.publicBaseUrl ?? `http://localhost:${options.port ?? 4201}` | ||
| ); | ||
| const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl ?? publicBaseUrl); | ||
| const iframeAllowedOrigins = (options.allowedOrigins ?? [publicBaseUrl]).map( | ||
| (origin) => urlOrigin(origin) | ||
| ); | ||
| const iframePath = options.iframePath ?? "/shadow/server"; | ||
| const iconPath = options.iconPath ?? "/assets/icon.svg"; | ||
| const sourceAssetOrigin = urlOrigin(manifest.iconUrl); | ||
| return { | ||
| ...manifest, | ||
| iconUrl: joinBasePath(publicBaseUrl, iconPath), | ||
| marketplace: manifest.marketplace ? { | ||
| ...manifest.marketplace, | ||
| coverImageUrl: manifest.marketplace.coverImageUrl ? rebasePublicAssetUrl( | ||
| manifest.marketplace.coverImageUrl, | ||
| sourceAssetOrigin, | ||
| publicBaseUrl | ||
| ) : manifest.marketplace.coverImageUrl, | ||
| gallery: manifest.marketplace.gallery?.map((item) => ({ | ||
| ...item, | ||
| url: rebasePublicAssetUrl(item.url, sourceAssetOrigin, publicBaseUrl) | ||
| })), | ||
| links: manifest.marketplace.links?.map((item) => ({ | ||
| ...item, | ||
| url: rebasePublicAssetUrl(item.url, sourceAssetOrigin, publicBaseUrl) | ||
| })) | ||
| } : manifest.marketplace, | ||
| iframe: manifest.iframe ? { | ||
| ...manifest.iframe, | ||
| entry: joinBasePath(publicBaseUrl, iframePath), | ||
| allowedOrigins: iframeAllowedOrigins | ||
| } : manifest.iframe, | ||
| api: { | ||
| ...manifest.api, | ||
| baseUrl: apiBaseUrl | ||
| } | ||
| }; | ||
| } | ||
| function defineShadowServerApp(manifest, options = {}) { | ||
| return new ShadowServerAppRuntime(manifest, options); | ||
| } | ||
| var createShadowServerAppRuntime = defineShadowServerApp; | ||
| var ShadowServerAppCommandError = class extends Error { | ||
| status; | ||
| issues; | ||
| constructor(status, error, issues) { | ||
| super(error); | ||
| this.name = "ShadowServerAppCommandError"; | ||
| this.status = status; | ||
| this.issues = issues; | ||
| } | ||
| }; | ||
| function shadowServerAppError(status, error, issues) { | ||
| return new ShadowServerAppCommandError(status, error, issues); | ||
| } | ||
| var ShadowServerAppRuntime = class { | ||
| constructor(sourceManifest, options = {}) { | ||
| this.sourceManifest = sourceManifest; | ||
| this.options = options; | ||
| } | ||
| sourceManifest; | ||
| options; | ||
| manifest(options = {}) { | ||
| return createShadowServerAppManifest(this.sourceManifest, options); | ||
| } | ||
| defineCommands(handlers) { | ||
| return handlers; | ||
| } | ||
| actor(envelopeOrContext) { | ||
| return shadowServerAppActorRef(envelopeOrContext); | ||
| } | ||
| error(status, error, issues) { | ||
| return shadowServerAppError(status, error, issues); | ||
| } | ||
| async parseCommand(commandName, request) { | ||
| return parseShadowServerAppCommandRequest( | ||
| { | ||
| ...request, | ||
| expectedCommand: commandName, | ||
| shadowBaseUrl: this.options.shadowBaseUrl, | ||
| fetchImpl: this.options.fetchImpl | ||
| } | ||
| ); | ||
| } | ||
| async executeCommand(commandName, request, handlers) { | ||
| const parsed = await this.parseCommand(commandName, request); | ||
| if (!parsed.ok) return parseErrorResult(parsed); | ||
| return this.executeEnvelope(commandName, parsed.envelope, handlers); | ||
| } | ||
| async executeLocal(commandName, input, context, handlers) { | ||
| return this.executeEnvelope( | ||
| commandName, | ||
| { | ||
| input, | ||
| context: { | ||
| ...context, | ||
| command: commandName | ||
| } | ||
| }, | ||
| handlers | ||
| ); | ||
| } | ||
| async executeEnvelope(commandName, envelope, handlers) { | ||
| const command = this.sourceManifest.commands.find((item) => item.name === commandName); | ||
| if (!command) return failureResult(404, "command_not_found"); | ||
| const validation = validateShadowServerAppJsonSchema(command.inputSchema, envelope.input); | ||
| if (!validation.ok) return failureResult(422, "invalid_input", validation.issues); | ||
| const handler = handlers[commandName]; | ||
| if (!handler) return failureResult(404, "command_not_found"); | ||
| try { | ||
| const result = await handler(envelope.input, { | ||
| context: envelope.context, | ||
| actor: this.actor(envelope) | ||
| }); | ||
| return { ok: true, status: 200, body: { ok: true, result } }; | ||
| } catch (error) { | ||
| if (error instanceof ShadowServerAppCommandError) { | ||
| return failureResult(error.status, error.message, error.issues); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| }; | ||
| async function introspectShadowServerAppToken(input) { | ||
| const baseUrl = trimTrailingSlash(input.shadowBaseUrl ?? "http://localhost:3002"); | ||
| const fetchImpl = input.fetchImpl ?? fetch; | ||
| const response = await fetchImpl( | ||
| `${baseUrl}/api/servers/${encodeURIComponent(input.serverId)}/apps/${encodeURIComponent( | ||
| input.appKey | ||
| )}/oauth/introspect`, | ||
| { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${input.token}`, | ||
| "Content-Type": "application/json" | ||
| }, | ||
| body: JSON.stringify({ token: input.token }) | ||
| } | ||
| ); | ||
| if (!response.ok) return null; | ||
| const payload = await response.json(); | ||
| return payload.active ? payload : null; | ||
| } | ||
| async function parseShadowServerAppCommandRequest(input) { | ||
| const token = extractShadowServerAppBearerToken(input.authorizationHeader); | ||
| const serverId = input.serverIdHeader; | ||
| const appKey = input.appKeyHeader; | ||
| if (!token || !serverId || !appKey) { | ||
| return { ok: false, status: 401, error: "missing_oauth" }; | ||
| } | ||
| const introspection = await introspectShadowServerAppToken({ | ||
| token, | ||
| serverId, | ||
| appKey, | ||
| shadowBaseUrl: input.shadowBaseUrl, | ||
| fetchImpl: input.fetchImpl | ||
| }).catch(() => null); | ||
| const context = introspection?.shadow; | ||
| if (!context) return { ok: false, status: 401, error: "invalid_token" }; | ||
| if (context.command !== input.expectedCommand) { | ||
| return { ok: false, status: 403, error: "wrong_command" }; | ||
| } | ||
| let commandInput; | ||
| if (input.requestInput !== void 0) { | ||
| commandInput = input.requestInput; | ||
| } else { | ||
| let body; | ||
| try { | ||
| body = JSON.parse(input.requestBody ?? "{}"); | ||
| } catch { | ||
| return { ok: false, status: 400, error: "invalid_json" }; | ||
| } | ||
| commandInput = body.input ?? {}; | ||
| } | ||
| return { | ||
| ok: true, | ||
| envelope: { | ||
| input: commandInput, | ||
| context | ||
| } | ||
| }; | ||
| } | ||
| function validateShadowServerAppJsonSchema(schema, value) { | ||
| if (!schema) return { ok: true }; | ||
| const issues = []; | ||
| validateJsonSchemaValue(schema, value, "", issues); | ||
| return issues.length ? { ok: false, issues } : { ok: true }; | ||
| } | ||
| function shadowServerAppActorDisplayName(envelopeOrContext) { | ||
| const context = "context" in envelopeOrContext ? envelopeOrContext.context : envelopeOrContext; | ||
| const actor = context.actor; | ||
| const profile = actor.profile; | ||
| return profile?.displayName?.trim() || profile?.username?.trim() || (actor.buddyAgentId ? `Buddy ${actor.buddyAgentId.slice(0, 8)}` : null) || (actor.userId ? `${actor.kind}:${actor.userId.slice(0, 8)}` : null) || `${actor.kind}:unknown`; | ||
| } | ||
| function shadowServerAppActorAvatarUrl(envelopeOrContext) { | ||
| const context = "context" in envelopeOrContext ? envelopeOrContext.context : envelopeOrContext; | ||
| return context.actor.profile?.avatarUrl ?? null; | ||
| } | ||
| function shadowServerAppActorRef(envelopeOrContext) { | ||
| const context = "context" in envelopeOrContext ? envelopeOrContext.context : envelopeOrContext; | ||
| const actor = context.actor; | ||
| return { | ||
| kind: actor.kind, | ||
| id: actor.buddyAgentId ?? actor.userId ?? actor.ownerId ?? "unknown", | ||
| userId: actor.userId ?? null, | ||
| buddyAgentId: actor.buddyAgentId ?? null, | ||
| ownerId: actor.ownerId ?? null, | ||
| displayName: shadowServerAppActorDisplayName(context), | ||
| avatarUrl: shadowServerAppActorAvatarUrl(context) | ||
| }; | ||
| } | ||
| function parseErrorResult(error) { | ||
| return failureResult(error.status, error.error, error.issues); | ||
| } | ||
| function failureResult(status, error, issues) { | ||
| return { | ||
| ok: false, | ||
| status, | ||
| body: issues === void 0 ? { ok: false, error } : { ok: false, error, issues } | ||
| }; | ||
| } | ||
| function validateJsonSchemaValue(schema, value, path, issues) { | ||
| if (Array.isArray(schema.oneOf)) { | ||
| const matches = schema.oneOf.some((option) => { | ||
| const nestedIssues = []; | ||
| if (option && typeof option === "object" && !Array.isArray(option)) { | ||
| validateJsonSchemaValue( | ||
| option, | ||
| value, | ||
| path, | ||
| nestedIssues | ||
| ); | ||
| } | ||
| return nestedIssues.length === 0; | ||
| }); | ||
| if (!matches) issues.push({ path, message: "Expected value matching one schema option" }); | ||
| return; | ||
| } | ||
| const enumValues = schema.enum; | ||
| if (Array.isArray(enumValues) && !enumValues.includes(value)) { | ||
| issues.push({ path, message: `Expected one of ${enumValues.map(String).join(", ")}` }); | ||
| return; | ||
| } | ||
| const type = schema.type; | ||
| if (type === "object") { | ||
| if (!value || typeof value !== "object" || Array.isArray(value)) { | ||
| issues.push({ path, message: "Expected object" }); | ||
| return; | ||
| } | ||
| const record = value; | ||
| const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? schema.properties : {}; | ||
| const required = Array.isArray(schema.required) ? schema.required.map(String) : []; | ||
| for (const key of required) { | ||
| if (!(key in record)) issues.push({ path: joinJsonPath(path, key), message: "Required" }); | ||
| } | ||
| for (const [key, propertySchema] of Object.entries(properties)) { | ||
| if (record[key] !== void 0) { | ||
| validateJsonSchemaValue(propertySchema, record[key], joinJsonPath(path, key), issues); | ||
| } | ||
| } | ||
| const additionalProperties = schema.additionalProperties && typeof schema.additionalProperties === "object" && !Array.isArray(schema.additionalProperties) ? schema.additionalProperties : null; | ||
| if (additionalProperties) { | ||
| for (const [key, nestedValue] of Object.entries(record)) { | ||
| if (!(key in properties)) { | ||
| validateJsonSchemaValue( | ||
| additionalProperties, | ||
| nestedValue, | ||
| joinJsonPath(path, key), | ||
| issues | ||
| ); | ||
| } | ||
| } | ||
| } else if (schema.additionalProperties === false) { | ||
| for (const key of Object.keys(record)) { | ||
| if (!(key in properties)) { | ||
| issues.push({ path: joinJsonPath(path, key), message: "Unknown property" }); | ||
| } | ||
| } | ||
| } | ||
| return; | ||
| } | ||
| if (type === "array") { | ||
| if (!Array.isArray(value)) { | ||
| issues.push({ path, message: "Expected array" }); | ||
| return; | ||
| } | ||
| const maxItems = typeof schema.maxItems === "number" ? schema.maxItems : null; | ||
| if (maxItems !== null && value.length > maxItems) { | ||
| issues.push({ path, message: `Expected at most ${maxItems} items` }); | ||
| } | ||
| const itemSchema = schema.items && typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : null; | ||
| if (itemSchema) { | ||
| value.forEach( | ||
| (item, index) => validateJsonSchemaValue(itemSchema, item, `${path}[${index}]`, issues) | ||
| ); | ||
| } | ||
| return; | ||
| } | ||
| if (type === "string") { | ||
| if (typeof value !== "string") { | ||
| issues.push({ path, message: "Expected string" }); | ||
| return; | ||
| } | ||
| const maxLength = typeof schema.maxLength === "number" ? schema.maxLength : null; | ||
| const minLength = typeof schema.minLength === "number" ? schema.minLength : null; | ||
| if (minLength !== null && value.length < minLength) { | ||
| issues.push({ path, message: `Expected at least ${minLength} characters` }); | ||
| } | ||
| if (maxLength !== null && value.length > maxLength) { | ||
| issues.push({ path, message: `Expected at most ${maxLength} characters` }); | ||
| } | ||
| return; | ||
| } | ||
| if (type === "number" || type === "integer") { | ||
| if (typeof value !== "number" || !Number.isFinite(value)) { | ||
| issues.push({ path, message: "Expected number" }); | ||
| return; | ||
| } | ||
| if (type === "integer" && !Number.isInteger(value)) { | ||
| issues.push({ path, message: "Expected integer" }); | ||
| } | ||
| return; | ||
| } | ||
| if (type === "boolean" && typeof value !== "boolean") { | ||
| issues.push({ path, message: "Expected boolean" }); | ||
| } | ||
| } | ||
| function joinJsonPath(parent, key) { | ||
| return parent ? `${parent}.${key}` : key; | ||
| } | ||
| export { | ||
| SHADOW_SERVER_APP_PROTOCOL, | ||
| SHADOW_SERVER_APP_COMMAND_COMPLETED_EVENT, | ||
| SHADOW_SERVER_APP_COMMAND_FAILED_EVENT, | ||
| SHADOW_SERVER_APP_COMMAND_EVENTS, | ||
| ShadowServerAppHttpError, | ||
| shadowServerAppInboxTaskEndpoint, | ||
| buildShadowServerAppInboxTaskRequest, | ||
| getShadowServerAppTaskCardId, | ||
| buildShadowServerAppInboxDelivery, | ||
| getShadowServerAppInboxDeliveries, | ||
| getShadowServerAppInboxErrors, | ||
| getShadowServerAppPendingInboxTasks, | ||
| getShadowServerAppChannelMessageDeliveries, | ||
| getShadowServerAppChannelMessageErrors, | ||
| getShadowServerAppPendingChannelMessages, | ||
| hasShadowServerAppPendingOutbox, | ||
| unwrapShadowServerAppCommandPayload, | ||
| readShadowServerAppCommandResponse, | ||
| ShadowServerAppOutbox, | ||
| extractShadowServerAppBearerToken, | ||
| decodeShadowServerAppLaunchTokenHint, | ||
| fetchShadowServerAppLaunchInboxes, | ||
| deliverShadowServerAppLaunchOutbox, | ||
| normalizeShadowServerAppCommandInput, | ||
| createShadowServerAppManifest, | ||
| defineShadowServerApp, | ||
| createShadowServerAppRuntime, | ||
| ShadowServerAppCommandError, | ||
| shadowServerAppError, | ||
| ShadowServerAppRuntime, | ||
| introspectShadowServerAppToken, | ||
| parseShadowServerAppCommandRequest, | ||
| validateShadowServerAppJsonSchema, | ||
| shadowServerAppActorDisplayName, | ||
| shadowServerAppActorAvatarUrl, | ||
| shadowServerAppActorRef, | ||
| BUDDY_INBOX_DELIVERY_PERMISSION | ||
| }; |
| import { | ||
| BUDDY_INBOX_DELIVERY_PERMISSION, | ||
| decodeShadowServerAppLaunchTokenHint, | ||
| deliverShadowServerAppLaunchOutbox, | ||
| getShadowServerAppChannelMessageDeliveries, | ||
| getShadowServerAppChannelMessageErrors, | ||
| getShadowServerAppInboxDeliveries, | ||
| getShadowServerAppInboxErrors, | ||
| hasShadowServerAppPendingOutbox, | ||
| readShadowServerAppCommandResponse, | ||
| unwrapShadowServerAppCommandPayload | ||
| } from "./chunk-N55V4ZKC.js"; | ||
| // src/bridge.ts | ||
| var SHADOW_BRIDGE_CAPABILITIES = [ | ||
| "copilot.open", | ||
| "workspace.open", | ||
| "buddy.create.open", | ||
| "buddy.inboxes.list", | ||
| "buddy.grant.ensure", | ||
| "oauth.authorize", | ||
| "route.navigate" | ||
| ]; | ||
| function commandPath(basePath, commandName) { | ||
| return `${basePath.replace(/\/+$/u, "")}/${encodeURIComponent(commandName)}`; | ||
| } | ||
| function shadowServerAppMountedPathPrefix(windowRef) { | ||
| const win = windowRef ?? (typeof window === "undefined" ? null : window); | ||
| const pathname = win?.location?.pathname ?? ""; | ||
| const segments = pathname.split("/").filter(Boolean); | ||
| const shadowIndex = segments.indexOf("shadow"); | ||
| if (shadowIndex <= 0 || segments[shadowIndex + 1] !== "server") return ""; | ||
| return `/${segments.slice(0, shadowIndex).join("/")}`; | ||
| } | ||
| function shadowServerAppMountedPath(path, windowRef) { | ||
| const normalized = path.startsWith("/") ? path : `/${path}`; | ||
| return `${shadowServerAppMountedPathPrefix(windowRef)}${normalized}`; | ||
| } | ||
| function isRecord(value) { | ||
| return !!value && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| function withoutUndefined(value) { | ||
| if (value === void 0) return {}; | ||
| if (Array.isArray(value)) return value.map(withoutUndefined); | ||
| if (!isRecord(value)) return value; | ||
| return Object.fromEntries( | ||
| Object.entries(value).filter(([, entry]) => entry !== void 0).map(([key, entry]) => [key, withoutUndefined(entry)]) | ||
| ); | ||
| } | ||
| var ShadowServerAppBrowserClient = class { | ||
| bridge; | ||
| commandBasePath; | ||
| inboxesPath; | ||
| shadowApiBaseUrl; | ||
| fetchFn; | ||
| deliverLaunchOutboxFromBrowser; | ||
| win; | ||
| constructor(options = {}) { | ||
| this.bridge = new ShadowBridge(options); | ||
| const windowRef = options.windowRef ?? (typeof window === "undefined" ? null : window); | ||
| this.commandBasePath = options.commandBasePath ?? shadowServerAppMountedPath("/api/local/commands", windowRef); | ||
| this.inboxesPath = options.inboxesPath ?? shadowServerAppMountedPath("/api/local/inboxes", windowRef); | ||
| this.shadowApiBaseUrl = options.shadowApiBaseUrl; | ||
| this.fetchFn = options.fetch; | ||
| this.deliverLaunchOutboxFromBrowser = options.deliverLaunchOutboxFromBrowser ?? false; | ||
| this.win = windowRef; | ||
| } | ||
| bridgeAvailable() { | ||
| return this.bridge.isAvailable(); | ||
| } | ||
| launchToken(param = "shadow_launch") { | ||
| if (!this.win) return null; | ||
| return new URLSearchParams(this.win.location.search).get(param); | ||
| } | ||
| launchHeaders(headers = {}, options = {}) { | ||
| const token = this.launchToken(options.launchTokenParam); | ||
| return token ? { ...headers, "X-Shadow-Launch-Token": token } : headers; | ||
| } | ||
| async command(commandName, input = {}) { | ||
| const response = await this.fetch(commandPath(this.commandBasePath, commandName), { | ||
| method: "POST", | ||
| headers: this.launchHeaders({ "Content-Type": "application/json" }), | ||
| body: JSON.stringify({ input: withoutUndefined(input) }) | ||
| }); | ||
| const result = await readShadowServerAppCommandResponse(response); | ||
| return this.deliverLaunchOutbox(commandName, result); | ||
| } | ||
| async listBuddyInboxes(options = {}) { | ||
| if (this.bridge.isAvailable()) { | ||
| try { | ||
| return await this.bridge.listBuddyInboxes({ refresh: options.refresh }); | ||
| } catch { | ||
| } | ||
| } | ||
| const response = await this.fetch(this.inboxesPath, { headers: this.launchHeaders() }); | ||
| if (!response.ok) { | ||
| if (options.emptyOnError) return { inboxes: [] }; | ||
| const message = await response.text().catch(() => ""); | ||
| throw new Error(message || `Buddy inbox lookup failed (${response.status})`); | ||
| } | ||
| return await response.json(); | ||
| } | ||
| async ensureBuddyTaskGrant(input) { | ||
| const buddyAgentId = input.agentId?.trim(); | ||
| if (!buddyAgentId || !this.bridge.isAvailable()) return { granted: false, skipped: true }; | ||
| return this.bridge.ensureBuddyGrant( | ||
| { | ||
| buddyAgentId, | ||
| permissions: input.permissions ?? [BUDDY_INBOX_DELIVERY_PERMISSION], | ||
| reason: input.reason | ||
| }, | ||
| { timeoutMs: input.timeoutMs ?? 6e4 } | ||
| ); | ||
| } | ||
| openBuddyCreator(input = {}, options = {}) { | ||
| if (!this.bridge.isAvailable()) return Promise.resolve({ opened: false, agent: null }); | ||
| return this.bridge.openBuddyCreator(input, options); | ||
| } | ||
| openCopilot(delivery, options = {}) { | ||
| return this.bridge.openCopilot(delivery, options); | ||
| } | ||
| openWorkspaceResource(input, options = {}) { | ||
| return this.bridge.openWorkspaceResource(input, options); | ||
| } | ||
| authorizeOAuth(input, options = {}) { | ||
| if (!this.bridge.isAvailable()) return Promise.resolve({ opened: false }); | ||
| return this.bridge.authorizeOAuth(input, options); | ||
| } | ||
| inboxDeliveries(payload) { | ||
| return this.bridge.inboxDeliveries(payload); | ||
| } | ||
| inboxErrors(payload) { | ||
| return this.bridge.inboxErrors(payload); | ||
| } | ||
| channelMessageDeliveries(payload) { | ||
| return this.bridge.channelMessageDeliveries(payload); | ||
| } | ||
| channelMessageErrors(payload) { | ||
| return this.bridge.channelMessageErrors(payload); | ||
| } | ||
| async deliverLaunchOutbox(commandName, result) { | ||
| if (!this.deliverLaunchOutboxFromBrowser || !hasShadowServerAppPendingOutbox(result)) { | ||
| return result; | ||
| } | ||
| const launchToken = this.launchToken(); | ||
| if (!decodeShadowServerAppLaunchTokenHint(launchToken)) return result; | ||
| return await deliverShadowServerAppLaunchOutbox({ | ||
| commandName, | ||
| result, | ||
| launchToken, | ||
| shadowApiBaseUrl: this.shadowApiBaseUrl, | ||
| fetch: this.fetch.bind(this) | ||
| }); | ||
| } | ||
| fetch(input, init) { | ||
| if (this.fetchFn) return this.fetchFn(input, init); | ||
| return globalThis.fetch(input, init); | ||
| } | ||
| }; | ||
| function createShadowServerAppClient(options = {}) { | ||
| return new ShadowServerAppBrowserClient(options); | ||
| } | ||
| var createShadowServerAppBrowserClient = createShadowServerAppClient; | ||
| function createShadowServerAppRuntimeClient(options = {}) { | ||
| const windowRef = options.windowRef ?? (typeof window === "undefined" ? void 0 : window); | ||
| return new ShadowServerAppBrowserClient({ | ||
| ...options, | ||
| windowRef, | ||
| commandBasePath: options.commandBasePath ?? shadowServerAppMountedPath("/api/runtime/commands", windowRef), | ||
| inboxesPath: options.inboxesPath ?? shadowServerAppMountedPath("/api/runtime/inboxes", windowRef), | ||
| deliverLaunchOutboxFromBrowser: false | ||
| }); | ||
| } | ||
| var ShadowBridge = class _ShadowBridge { | ||
| static capabilitiesRequestType = "shadow.app.capabilities.request"; | ||
| static capabilitiesResponseType = "shadow.app.capabilities.response"; | ||
| static openCopilotRequestType = "shadow.app.copilot.open.request"; | ||
| static openCopilotResponseType = "shadow.app.copilot.open.response"; | ||
| static openWorkspaceResourceRequestType = "shadow.app.workspace.open.request"; | ||
| static openWorkspaceResourceResponseType = "shadow.app.workspace.open.response"; | ||
| static openBuddyCreatorRequestType = "shadow.app.buddy.create.request"; | ||
| static openBuddyCreatorResponseType = "shadow.app.buddy.create.response"; | ||
| static listBuddyInboxesRequestType = "shadow.app.buddy.inboxes.request"; | ||
| static listBuddyInboxesResponseType = "shadow.app.buddy.inboxes.response"; | ||
| static ensureBuddyGrantRequestType = "shadow.app.buddy.grant.request"; | ||
| static ensureBuddyGrantResponseType = "shadow.app.buddy.grant.response"; | ||
| static authorizeOAuthRequestType = "shadow.app.oauth.authorize.request"; | ||
| static authorizeOAuthResponseType = "shadow.app.oauth.authorize.response"; | ||
| static inboxDeliveries(payload) { | ||
| return getShadowServerAppInboxDeliveries(payload); | ||
| } | ||
| static inboxErrors(payload) { | ||
| return getShadowServerAppInboxErrors(payload); | ||
| } | ||
| static channelMessageDeliveries(payload) { | ||
| return getShadowServerAppChannelMessageDeliveries(payload); | ||
| } | ||
| static channelMessageErrors(payload) { | ||
| return getShadowServerAppChannelMessageErrors(payload); | ||
| } | ||
| static unwrapCommandPayload(payload) { | ||
| return unwrapShadowServerAppCommandPayload(payload); | ||
| } | ||
| appKey; | ||
| targetOrigin; | ||
| timeoutMs; | ||
| win; | ||
| hasLaunchContext; | ||
| pending = /* @__PURE__ */ new Map(); | ||
| onMessage = (event) => { | ||
| let data = event.data; | ||
| if (typeof data === "string") { | ||
| try { | ||
| data = JSON.parse(data || "{}"); | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
| if (!data || typeof data !== "object") return; | ||
| const record = data; | ||
| if (typeof record.requestId !== "string" || typeof record.type !== "string") return; | ||
| const entry = this.pending.get(record.requestId); | ||
| if (!entry || record.type !== entry.responseType) return; | ||
| this.pending.delete(record.requestId); | ||
| if (record.ok) entry.resolve(record.result); | ||
| else | ||
| entry.reject( | ||
| new Error(typeof record.error === "string" ? record.error : "Bridge request failed") | ||
| ); | ||
| }; | ||
| constructor(options = {}) { | ||
| this.win = options.windowRef ?? (typeof window === "undefined" ? null : window); | ||
| this.appKey = options.appKey ?? this.resolveLaunchAppKey(); | ||
| this.targetOrigin = options.targetOrigin ?? "*"; | ||
| this.timeoutMs = options.timeoutMs ?? 6e4; | ||
| this.hasLaunchContext = this.resolveLaunchContext(); | ||
| this.win?.addEventListener("message", this.onMessage); | ||
| } | ||
| dispose() { | ||
| this.win?.removeEventListener("message", this.onMessage); | ||
| for (const entry of this.pending.values()) { | ||
| entry.reject(new Error("ShadowBridge disposed")); | ||
| } | ||
| this.pending.clear(); | ||
| } | ||
| isAvailable() { | ||
| if (!this.win) return false; | ||
| return this.hasLaunchContext && (this.win.parent !== this.win || !!this.win.ReactNativeWebView); | ||
| } | ||
| capabilities(options = {}) { | ||
| return this.request( | ||
| _ShadowBridge.capabilitiesRequestType, | ||
| _ShadowBridge.capabilitiesResponseType, | ||
| {}, | ||
| options.timeoutMs ?? 15e3 | ||
| ); | ||
| } | ||
| openCopilot(deliveryOrInput, options = {}) { | ||
| const input = "delivery" in deliveryOrInput ? deliveryOrInput : { delivery: deliveryOrInput }; | ||
| return this.request( | ||
| _ShadowBridge.openCopilotRequestType, | ||
| _ShadowBridge.openCopilotResponseType, | ||
| input, | ||
| options.timeoutMs ?? 15e3 | ||
| ); | ||
| } | ||
| openWorkspaceResource(input, options = {}) { | ||
| return this.request( | ||
| _ShadowBridge.openWorkspaceResourceRequestType, | ||
| _ShadowBridge.openWorkspaceResourceResponseType, | ||
| input, | ||
| options.timeoutMs ?? 15e3 | ||
| ); | ||
| } | ||
| openBuddyCreator(input = {}, options = {}) { | ||
| return this.request( | ||
| _ShadowBridge.openBuddyCreatorRequestType, | ||
| _ShadowBridge.openBuddyCreatorResponseType, | ||
| input, | ||
| options.timeoutMs ?? 10 * 60 * 1e3 | ||
| ); | ||
| } | ||
| listBuddyInboxes(input = {}, options = {}) { | ||
| return this.request( | ||
| _ShadowBridge.listBuddyInboxesRequestType, | ||
| _ShadowBridge.listBuddyInboxesResponseType, | ||
| input, | ||
| options.timeoutMs ?? 15e3 | ||
| ); | ||
| } | ||
| ensureBuddyGrant(input, options = {}) { | ||
| return this.request( | ||
| _ShadowBridge.ensureBuddyGrantRequestType, | ||
| _ShadowBridge.ensureBuddyGrantResponseType, | ||
| input, | ||
| options.timeoutMs ?? 3e4 | ||
| ); | ||
| } | ||
| authorizeOAuth(input, options = {}) { | ||
| const payload = typeof input === "string" ? { authorizeUrl: input } : input; | ||
| return this.request( | ||
| _ShadowBridge.authorizeOAuthRequestType, | ||
| _ShadowBridge.authorizeOAuthResponseType, | ||
| payload, | ||
| options.timeoutMs ?? 10 * 60 * 1e3 | ||
| ); | ||
| } | ||
| unwrapCommandPayload(payload) { | ||
| return unwrapShadowServerAppCommandPayload(payload); | ||
| } | ||
| inboxDeliveries(payload) { | ||
| return getShadowServerAppInboxDeliveries(payload); | ||
| } | ||
| inboxErrors(payload) { | ||
| return getShadowServerAppInboxErrors(payload); | ||
| } | ||
| channelMessageDeliveries(payload) { | ||
| return getShadowServerAppChannelMessageDeliveries(payload); | ||
| } | ||
| channelMessageErrors(payload) { | ||
| return getShadowServerAppChannelMessageErrors(payload); | ||
| } | ||
| request(requestType, responseType, payload, timeoutMs = this.timeoutMs) { | ||
| if (!this.isAvailable()) { | ||
| return Promise.reject( | ||
| new Error("ShadowBridge is not available outside a Shadow launch frame") | ||
| ); | ||
| } | ||
| const requestId = `req_${Math.random().toString(36).slice(2)}`; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(requestId, { | ||
| responseType, | ||
| resolve, | ||
| reject | ||
| }); | ||
| this.postMessage({ | ||
| type: requestType, | ||
| requestId, | ||
| ...this.appKey ? { appKey: this.appKey } : {}, | ||
| ...payload | ||
| }); | ||
| this.win?.setTimeout(() => { | ||
| if (!this.pending.has(requestId)) return; | ||
| this.pending.delete(requestId); | ||
| reject(new Error("Bridge request timed out")); | ||
| }, timeoutMs); | ||
| }); | ||
| } | ||
| postMessage(message) { | ||
| if (!this.win) return; | ||
| if (this.win.ReactNativeWebView) { | ||
| this.win.ReactNativeWebView.postMessage(JSON.stringify(message)); | ||
| return; | ||
| } | ||
| this.win.parent.postMessage(message, this.targetOrigin); | ||
| } | ||
| resolveLaunchContext() { | ||
| if (!this.win) return false; | ||
| const storageKey = this.appKey ? `shadow.bridge.launch:${this.appKey}` : null; | ||
| const memoryContexts = this.win.__shadowBridgeLaunchContexts ??= {}; | ||
| const hasLaunchToken = new URLSearchParams(this.win.location.search).has("shadow_launch"); | ||
| if (hasLaunchToken) { | ||
| if (this.appKey) memoryContexts[this.appKey] = true; | ||
| try { | ||
| if (storageKey) this.win.sessionStorage?.setItem(storageKey, "1"); | ||
| } catch { | ||
| } | ||
| return true; | ||
| } | ||
| if (this.appKey && memoryContexts[this.appKey]) return true; | ||
| try { | ||
| return storageKey ? this.win.sessionStorage?.getItem(storageKey) === "1" : false; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| resolveLaunchAppKey() { | ||
| if (!this.win) return void 0; | ||
| const token = new URLSearchParams(this.win.location.search).get("shadow_launch"); | ||
| return decodeShadowServerAppLaunchTokenHint(token)?.appKey; | ||
| } | ||
| }; | ||
| export { | ||
| SHADOW_BRIDGE_CAPABILITIES, | ||
| shadowServerAppMountedPathPrefix, | ||
| shadowServerAppMountedPath, | ||
| ShadowServerAppBrowserClient, | ||
| createShadowServerAppClient, | ||
| createShadowServerAppBrowserClient, | ||
| createShadowServerAppRuntimeClient, | ||
| ShadowBridge | ||
| }; |
+2
-2
@@ -10,3 +10,3 @@ import { | ||
| shadowServerAppMountedPathPrefix | ||
| } from "./chunk-L5PHQEDC.js"; | ||
| } from "./chunk-QX6PUTJZ.js"; | ||
| import { | ||
@@ -23,3 +23,3 @@ SHADOW_SERVER_APP_COMMAND_COMPLETED_EVENT, | ||
| shadowServerAppInboxTaskEndpoint | ||
| } from "./chunk-5ODYWMBC.js"; | ||
| } from "./chunk-N55V4ZKC.js"; | ||
| export { | ||
@@ -26,0 +26,0 @@ SHADOW_BRIDGE_CAPABILITIES, |
@@ -33,2 +33,3 @@ "use strict"; | ||
| } | ||
| options; | ||
| read() { | ||
@@ -35,0 +36,0 @@ if (!(0, import_node_fs.existsSync)(this.options.filePath)) { |
@@ -8,2 +8,3 @@ // src/server-app-node.ts | ||
| } | ||
| options; | ||
| read() { | ||
@@ -10,0 +11,0 @@ if (!existsSync(this.options.filePath)) { |
@@ -475,2 +475,4 @@ "use strict"; | ||
| } | ||
| sourceManifest; | ||
| options; | ||
| manifest(options = {}) { | ||
@@ -477,0 +479,0 @@ return createShadowServerAppManifest(this.sourceManifest, options); |
@@ -39,3 +39,3 @@ import { | ||
| validateShadowServerAppJsonSchema | ||
| } from "./chunk-5ODYWMBC.js"; | ||
| } from "./chunk-N55V4ZKC.js"; | ||
| export { | ||
@@ -42,0 +42,0 @@ BUDDY_INBOX_DELIVERY_PERMISSION, |
+2
-2
| { | ||
| "name": "@shadowob/sdk", | ||
| "version": "1.1.50", | ||
| "version": "1.1.51", | ||
| "description": "Shadow SDK — typed REST client and real-time Socket.IO event listener for Shadow servers", | ||
@@ -51,3 +51,3 @@ "license": "MIT", | ||
| "socket.io-client": "^4.8.1", | ||
| "@shadowob/shared": "1.1.50" | ||
| "@shadowob/shared": "1.1.51" | ||
| }, | ||
@@ -54,0 +54,0 @@ "peerDependencies": { |
| // src/server-app.ts | ||
| import { | ||
| BUDDY_INBOX_DELIVERY_PERMISSION | ||
| } from "@shadowob/shared"; | ||
| var SHADOW_SERVER_APP_PROTOCOL = "shadow.app/1"; | ||
| var SHADOW_SERVER_APP_COMMAND_COMPLETED_EVENT = "server_app.command.completed"; | ||
| var SHADOW_SERVER_APP_COMMAND_FAILED_EVENT = "server_app.command.failed"; | ||
| var SHADOW_SERVER_APP_COMMAND_EVENTS = [ | ||
| SHADOW_SERVER_APP_COMMAND_COMPLETED_EVENT, | ||
| SHADOW_SERVER_APP_COMMAND_FAILED_EVENT | ||
| ]; | ||
| var ShadowServerAppHttpError = class extends Error { | ||
| status; | ||
| payload; | ||
| constructor(status, message, payload) { | ||
| super(message); | ||
| this.name = "ShadowServerAppHttpError"; | ||
| this.status = status; | ||
| this.payload = payload; | ||
| } | ||
| }; | ||
| function isProtocolRecord(value) { | ||
| return !!value && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| function optionalProtocolString(value) { | ||
| return typeof value === "string" && value ? value : void 0; | ||
| } | ||
| function protocolPathSegment(value) { | ||
| return encodeURIComponent(value); | ||
| } | ||
| function shadowServerAppInboxTaskEndpoint(serverIdOrSlug, target) { | ||
| if ("channelId" in target && target.channelId) { | ||
| return `/api/channels/${protocolPathSegment(target.channelId)}/inbox/tasks`; | ||
| } | ||
| if ("agentId" in target && target.agentId) { | ||
| return `/api/servers/${protocolPathSegment(serverIdOrSlug)}/inboxes/${protocolPathSegment( | ||
| target.agentId | ||
| )}/tasks`; | ||
| } | ||
| throw new Error("Missing Inbox task target"); | ||
| } | ||
| function buildShadowServerAppInboxTaskRequest(input) { | ||
| const appId = input.app.id ?? input.app.appId ?? input.app.appKey; | ||
| const serverAppData = isProtocolRecord(input.task.data?.serverApp) ? input.task.data.serverApp : {}; | ||
| return { | ||
| endpoint: shadowServerAppInboxTaskEndpoint(input.serverIdOrSlug, input.target), | ||
| body: { | ||
| title: input.task.title, | ||
| body: input.task.body, | ||
| priority: input.task.priority, | ||
| tags: input.task.tags, | ||
| idempotencyKey: input.task.idempotencyKey, | ||
| requirements: input.task.requirements, | ||
| outputContract: input.task.outputContract, | ||
| privacy: input.task.privacy, | ||
| app: { | ||
| id: appId, | ||
| appId, | ||
| appKey: input.app.appKey, | ||
| name: input.app.name ?? input.app.label ?? input.app.appKey, | ||
| label: input.app.label ?? input.app.name ?? input.app.appKey, | ||
| ...input.app.iconUrl ? { iconUrl: input.app.iconUrl } : {} | ||
| }, | ||
| source: { | ||
| kind: "server_app", | ||
| id: appId, | ||
| appId, | ||
| appKey: input.app.appKey, | ||
| ...input.app.name ? { appName: input.app.name } : {}, | ||
| ...input.app.iconUrl ? { iconUrl: input.app.iconUrl } : {}, | ||
| ...input.app.serverId ? { serverId: input.app.serverId } : {}, | ||
| ...input.commandName ? { command: input.commandName } : {}, | ||
| label: input.app.label ?? input.app.name ?? input.app.appKey, | ||
| ...input.task.resource ? { resource: input.task.resource } : {} | ||
| }, | ||
| data: { | ||
| ...input.task.data ?? {}, | ||
| serverApp: { | ||
| ...serverAppData, | ||
| appKey: input.app.appKey, | ||
| name: input.app.name ?? input.app.label ?? input.app.appKey, | ||
| label: input.app.label ?? input.app.name ?? input.app.appKey, | ||
| ...input.app.iconUrl ? { iconUrl: input.app.iconUrl } : {}, | ||
| ...input.commandName ? { command: input.commandName } : {} | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| function getShadowServerAppTaskCardId(message) { | ||
| const metadata = isProtocolRecord(message) ? message.metadata : null; | ||
| const cards = isProtocolRecord(metadata) && Array.isArray(metadata.cards) ? metadata.cards : []; | ||
| for (const item of cards) { | ||
| if (!isProtocolRecord(item)) continue; | ||
| if (item.kind === "task" && typeof item.id === "string" && item.id) return item.id; | ||
| } | ||
| return null; | ||
| } | ||
| function buildShadowServerAppInboxDelivery(input) { | ||
| const message = isProtocolRecord(input.message) ? input.message : {}; | ||
| return { | ||
| ..."agentId" in input.target && input.target.agentId ? { agentId: input.target.agentId } : {}, | ||
| channelId: optionalProtocolString(message.channelId), | ||
| messageId: optionalProtocolString(message.id), | ||
| cardId: getShadowServerAppTaskCardId(message), | ||
| idempotencyKey: input.idempotencyKey | ||
| }; | ||
| } | ||
| function shadowFromPayload(payload) { | ||
| if (payload.protocol === SHADOW_SERVER_APP_PROTOCOL) { | ||
| return payload; | ||
| } | ||
| const shadow = isProtocolRecord(payload.shadow) ? payload.shadow : null; | ||
| if (shadow?.protocol === SHADOW_SERVER_APP_PROTOCOL) { | ||
| return shadow; | ||
| } | ||
| return null; | ||
| } | ||
| function mergeShadowResult(value, shadow) { | ||
| if (!shadow) return value; | ||
| const existing = shadowFromPayload(value); | ||
| if (!existing) return { ...value, shadow }; | ||
| return { | ||
| ...value, | ||
| shadow: { | ||
| protocol: SHADOW_SERVER_APP_PROTOCOL, | ||
| outbox: { | ||
| ...existing.outbox ?? {}, | ||
| ...shadow.outbox ?? {} | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| function getShadowServerAppInboxDeliveries(payload) { | ||
| if (!isProtocolRecord(payload)) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| return shadow?.outbox?.deliveries ?? []; | ||
| } | ||
| function getShadowServerAppInboxErrors(payload) { | ||
| if (!isProtocolRecord(payload)) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| return shadow?.outbox?.errors ?? []; | ||
| } | ||
| function getShadowServerAppPendingInboxTasks(payload, depth = 0) { | ||
| if (!isProtocolRecord(payload) || depth > 4) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| const tasks = shadow?.outbox?.inboxTasks ?? []; | ||
| return "result" in payload && payload.result !== void 0 ? [...tasks, ...getShadowServerAppPendingInboxTasks(payload.result, depth + 1)] : tasks; | ||
| } | ||
| function getShadowServerAppChannelMessageDeliveries(payload) { | ||
| if (!isProtocolRecord(payload)) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| return shadow?.outbox?.channelMessageDeliveries ?? []; | ||
| } | ||
| function getShadowServerAppChannelMessageErrors(payload) { | ||
| if (!isProtocolRecord(payload)) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| return shadow?.outbox?.channelMessageErrors ?? []; | ||
| } | ||
| function getShadowServerAppPendingChannelMessages(payload, depth = 0) { | ||
| if (!isProtocolRecord(payload) || depth > 4) return []; | ||
| const shadow = shadowFromPayload(payload); | ||
| const messages = shadow?.outbox?.channelMessages ?? []; | ||
| return "result" in payload && payload.result !== void 0 ? [...messages, ...getShadowServerAppPendingChannelMessages(payload.result, depth + 1)] : messages; | ||
| } | ||
| function hasShadowServerAppPendingOutbox(payload) { | ||
| return getShadowServerAppPendingInboxTasks(payload).length > 0 || getShadowServerAppPendingChannelMessages(payload).length > 0; | ||
| } | ||
| function isDomainResultWithEvents(payload) { | ||
| return Array.isArray(payload.events) && ("cursor" in payload || "result" in payload); | ||
| } | ||
| function isCommandPayloadEnvelope(payload) { | ||
| if (isDomainResultWithEvents(payload)) return false; | ||
| return payload.ok === true || payload.ok === false || shadowFromPayload(payload) !== null; | ||
| } | ||
| function unwrapShadowServerAppCommandPayload(payload) { | ||
| if (isProtocolRecord(payload) && payload.ok === false) { | ||
| throw new Error(typeof payload.error === "string" ? payload.error : "Command failed"); | ||
| } | ||
| if (isProtocolRecord(payload) && "result" in payload && payload.result !== void 0 && isCommandPayloadEnvelope(payload)) { | ||
| const nested = unwrapShadowServerAppCommandPayload(payload.result); | ||
| const shadow = shadowFromPayload(payload); | ||
| if (isProtocolRecord(nested)) return mergeShadowResult(nested, shadow); | ||
| return nested; | ||
| } | ||
| return payload; | ||
| } | ||
| async function readShadowServerAppResponsePayload(response) { | ||
| const text = await response.text().catch(() => ""); | ||
| if (!text.trim()) return null; | ||
| try { | ||
| return JSON.parse(text); | ||
| } catch { | ||
| if (!response.ok) return { ok: false, error: text }; | ||
| throw new ShadowServerAppHttpError(response.status, "Command returned invalid JSON", text); | ||
| } | ||
| } | ||
| function shadowServerAppResponseErrorMessage(status, payload, fallback = "Command failed") { | ||
| if (isProtocolRecord(payload) && typeof payload.error === "string" && payload.error) { | ||
| return payload.error; | ||
| } | ||
| if (typeof payload === "string" && payload.trim()) return payload; | ||
| return status ? `${fallback} (${status})` : fallback; | ||
| } | ||
| async function readShadowServerAppCommandResponse(response) { | ||
| const payload = await readShadowServerAppResponsePayload(response); | ||
| if (!response.ok || isProtocolRecord(payload) && payload.ok === false) { | ||
| throw new ShadowServerAppHttpError( | ||
| response.status, | ||
| shadowServerAppResponseErrorMessage(response.status, payload), | ||
| payload | ||
| ); | ||
| } | ||
| return unwrapShadowServerAppCommandPayload(payload); | ||
| } | ||
| var ShadowServerAppOutbox = class { | ||
| inboxTasks = []; | ||
| channelMessages = []; | ||
| enqueueInboxTask(task) { | ||
| this.inboxTasks.push(task); | ||
| return this; | ||
| } | ||
| enqueueInboxTasks(tasks) { | ||
| for (const task of tasks) this.enqueueInboxTask(task); | ||
| return this; | ||
| } | ||
| sendChannelMessage(message) { | ||
| this.channelMessages.push(message); | ||
| return this; | ||
| } | ||
| sendChannelMessages(messages) { | ||
| for (const message of messages) this.sendChannelMessage(message); | ||
| return this; | ||
| } | ||
| toShadow() { | ||
| return { | ||
| protocol: SHADOW_SERVER_APP_PROTOCOL, | ||
| outbox: { | ||
| ...this.inboxTasks.length > 0 ? { inboxTasks: [...this.inboxTasks] } : {}, | ||
| ...this.channelMessages.length > 0 ? { channelMessages: [...this.channelMessages] } : {} | ||
| } | ||
| }; | ||
| } | ||
| attachTo(result) { | ||
| return { ...result, shadow: this.toShadow() }; | ||
| } | ||
| }; | ||
| function trimTrailingSlash(value) { | ||
| return value.replace(/\/$/, ""); | ||
| } | ||
| function joinBasePath(baseUrl, path) { | ||
| const cleanBase = trimTrailingSlash(baseUrl); | ||
| const cleanPath = path.startsWith("/") ? path : `/${path}`; | ||
| return `${cleanBase}${cleanPath}`; | ||
| } | ||
| function urlOrigin(value) { | ||
| try { | ||
| return new URL(value).origin; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function rebasePublicAssetUrl(value, sourceOrigin, publicBaseUrl) { | ||
| if (!sourceOrigin) return value; | ||
| try { | ||
| const url = new URL(value); | ||
| if (url.origin !== sourceOrigin) return value; | ||
| return joinBasePath(publicBaseUrl, `${url.pathname}${url.search}${url.hash}`); | ||
| } catch { | ||
| return value; | ||
| } | ||
| } | ||
| function extractShadowServerAppBearerToken(value) { | ||
| if (!value) return null; | ||
| return value.toLowerCase().startsWith("bearer ") ? value.slice(7).trim() : null; | ||
| } | ||
| function decodeBase64UrlJson(value) { | ||
| try { | ||
| const normalized = value.replace(/-/g, "+").replace(/_/g, "/"); | ||
| const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "="); | ||
| const binary = globalThis.atob(padded); | ||
| const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); | ||
| return JSON.parse(new TextDecoder().decode(bytes)); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| function decodeShadowServerAppLaunchTokenHint(token) { | ||
| if (!token) return null; | ||
| const parts = token.split("."); | ||
| if (parts.length !== 3 || parts[0] !== "sat_v1") return null; | ||
| const payload = decodeBase64UrlJson(parts[1]); | ||
| if (typeof payload?.serverId !== "string" || typeof payload.appKey !== "string") return null; | ||
| return { serverId: payload.serverId, appKey: payload.appKey }; | ||
| } | ||
| async function fetchShadowServerAppLaunchInboxes(options) { | ||
| const hint = decodeShadowServerAppLaunchTokenHint(options.launchToken); | ||
| if (!hint || !options.launchToken) return { inboxes: [] }; | ||
| const fetchFn = options.fetch ?? fetch; | ||
| const baseUrl = trimTrailingSlash(options.shadowApiBaseUrl ?? "http://localhost:3002"); | ||
| const response = await fetchFn( | ||
| `${baseUrl}/api/servers/${encodeURIComponent(hint.serverId)}/apps/${encodeURIComponent( | ||
| hint.appKey | ||
| )}/launch/inboxes`, | ||
| { headers: { Authorization: `Bearer ${options.launchToken}` } } | ||
| ); | ||
| if (!response.ok) { | ||
| const message = await response.text().catch(() => ""); | ||
| throw new Error(`Shadow launch inbox lookup failed (${response.status}): ${message}`); | ||
| } | ||
| return await response.json(); | ||
| } | ||
| async function deliverShadowServerAppLaunchOutbox(options) { | ||
| const hint = decodeShadowServerAppLaunchTokenHint(options.launchToken); | ||
| if (!hint || !options.launchToken) return options.result; | ||
| const fetchFn = options.fetch ?? fetch; | ||
| const baseUrl = trimTrailingSlash(options.shadowApiBaseUrl ?? "http://localhost:3002"); | ||
| const response = await fetchFn( | ||
| `${baseUrl}/api/servers/${encodeURIComponent(hint.serverId)}/apps/${encodeURIComponent( | ||
| hint.appKey | ||
| )}/launch/outbox`, | ||
| { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${options.launchToken}`, | ||
| "Content-Type": "application/json" | ||
| }, | ||
| body: JSON.stringify({ | ||
| commandName: options.commandName, | ||
| result: options.result | ||
| }) | ||
| } | ||
| ); | ||
| if (!response.ok) { | ||
| const payload = await readShadowServerAppResponsePayload(response); | ||
| throw new ShadowServerAppHttpError( | ||
| response.status, | ||
| shadowServerAppResponseErrorMessage( | ||
| response.status, | ||
| payload, | ||
| "Shadow launch outbox delivery failed" | ||
| ), | ||
| payload | ||
| ); | ||
| } | ||
| return readShadowServerAppResponsePayload(response); | ||
| } | ||
| function normalizeShadowServerAppCommandInput(value) { | ||
| if (value && typeof value === "object" && !Array.isArray(value) && "input" in value && Object.keys(value).every((key) => key === "input" || key === "channelId")) { | ||
| return value.input ?? {}; | ||
| } | ||
| return value; | ||
| } | ||
| function createShadowServerAppManifest(manifest, options = {}) { | ||
| const publicBaseUrl = trimTrailingSlash( | ||
| options.publicBaseUrl ?? `http://localhost:${options.port ?? 4201}` | ||
| ); | ||
| const apiBaseUrl = trimTrailingSlash(options.apiBaseUrl ?? publicBaseUrl); | ||
| const iframeAllowedOrigins = (options.allowedOrigins ?? [publicBaseUrl]).map( | ||
| (origin) => urlOrigin(origin) | ||
| ); | ||
| const iframePath = options.iframePath ?? "/shadow/server"; | ||
| const iconPath = options.iconPath ?? "/assets/icon.svg"; | ||
| const sourceAssetOrigin = urlOrigin(manifest.iconUrl); | ||
| return { | ||
| ...manifest, | ||
| iconUrl: joinBasePath(publicBaseUrl, iconPath), | ||
| marketplace: manifest.marketplace ? { | ||
| ...manifest.marketplace, | ||
| coverImageUrl: manifest.marketplace.coverImageUrl ? rebasePublicAssetUrl( | ||
| manifest.marketplace.coverImageUrl, | ||
| sourceAssetOrigin, | ||
| publicBaseUrl | ||
| ) : manifest.marketplace.coverImageUrl, | ||
| gallery: manifest.marketplace.gallery?.map((item) => ({ | ||
| ...item, | ||
| url: rebasePublicAssetUrl(item.url, sourceAssetOrigin, publicBaseUrl) | ||
| })), | ||
| links: manifest.marketplace.links?.map((item) => ({ | ||
| ...item, | ||
| url: rebasePublicAssetUrl(item.url, sourceAssetOrigin, publicBaseUrl) | ||
| })) | ||
| } : manifest.marketplace, | ||
| iframe: manifest.iframe ? { | ||
| ...manifest.iframe, | ||
| entry: joinBasePath(publicBaseUrl, iframePath), | ||
| allowedOrigins: iframeAllowedOrigins | ||
| } : manifest.iframe, | ||
| api: { | ||
| ...manifest.api, | ||
| baseUrl: apiBaseUrl | ||
| } | ||
| }; | ||
| } | ||
| function defineShadowServerApp(manifest, options = {}) { | ||
| return new ShadowServerAppRuntime(manifest, options); | ||
| } | ||
| var createShadowServerAppRuntime = defineShadowServerApp; | ||
| var ShadowServerAppCommandError = class extends Error { | ||
| status; | ||
| issues; | ||
| constructor(status, error, issues) { | ||
| super(error); | ||
| this.name = "ShadowServerAppCommandError"; | ||
| this.status = status; | ||
| this.issues = issues; | ||
| } | ||
| }; | ||
| function shadowServerAppError(status, error, issues) { | ||
| return new ShadowServerAppCommandError(status, error, issues); | ||
| } | ||
| var ShadowServerAppRuntime = class { | ||
| constructor(sourceManifest, options = {}) { | ||
| this.sourceManifest = sourceManifest; | ||
| this.options = options; | ||
| } | ||
| manifest(options = {}) { | ||
| return createShadowServerAppManifest(this.sourceManifest, options); | ||
| } | ||
| defineCommands(handlers) { | ||
| return handlers; | ||
| } | ||
| actor(envelopeOrContext) { | ||
| return shadowServerAppActorRef(envelopeOrContext); | ||
| } | ||
| error(status, error, issues) { | ||
| return shadowServerAppError(status, error, issues); | ||
| } | ||
| async parseCommand(commandName, request) { | ||
| return parseShadowServerAppCommandRequest( | ||
| { | ||
| ...request, | ||
| expectedCommand: commandName, | ||
| shadowBaseUrl: this.options.shadowBaseUrl, | ||
| fetchImpl: this.options.fetchImpl | ||
| } | ||
| ); | ||
| } | ||
| async executeCommand(commandName, request, handlers) { | ||
| const parsed = await this.parseCommand(commandName, request); | ||
| if (!parsed.ok) return parseErrorResult(parsed); | ||
| return this.executeEnvelope(commandName, parsed.envelope, handlers); | ||
| } | ||
| async executeLocal(commandName, input, context, handlers) { | ||
| return this.executeEnvelope( | ||
| commandName, | ||
| { | ||
| input, | ||
| context: { | ||
| ...context, | ||
| command: commandName | ||
| } | ||
| }, | ||
| handlers | ||
| ); | ||
| } | ||
| async executeEnvelope(commandName, envelope, handlers) { | ||
| const command = this.sourceManifest.commands.find((item) => item.name === commandName); | ||
| if (!command) return failureResult(404, "command_not_found"); | ||
| const validation = validateShadowServerAppJsonSchema(command.inputSchema, envelope.input); | ||
| if (!validation.ok) return failureResult(422, "invalid_input", validation.issues); | ||
| const handler = handlers[commandName]; | ||
| if (!handler) return failureResult(404, "command_not_found"); | ||
| try { | ||
| const result = await handler(envelope.input, { | ||
| context: envelope.context, | ||
| actor: this.actor(envelope) | ||
| }); | ||
| return { ok: true, status: 200, body: { ok: true, result } }; | ||
| } catch (error) { | ||
| if (error instanceof ShadowServerAppCommandError) { | ||
| return failureResult(error.status, error.message, error.issues); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
| }; | ||
| async function introspectShadowServerAppToken(input) { | ||
| const baseUrl = trimTrailingSlash(input.shadowBaseUrl ?? "http://localhost:3002"); | ||
| const fetchImpl = input.fetchImpl ?? fetch; | ||
| const response = await fetchImpl( | ||
| `${baseUrl}/api/servers/${encodeURIComponent(input.serverId)}/apps/${encodeURIComponent( | ||
| input.appKey | ||
| )}/oauth/introspect`, | ||
| { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${input.token}`, | ||
| "Content-Type": "application/json" | ||
| }, | ||
| body: JSON.stringify({ token: input.token }) | ||
| } | ||
| ); | ||
| if (!response.ok) return null; | ||
| const payload = await response.json(); | ||
| return payload.active ? payload : null; | ||
| } | ||
| async function parseShadowServerAppCommandRequest(input) { | ||
| const token = extractShadowServerAppBearerToken(input.authorizationHeader); | ||
| const serverId = input.serverIdHeader; | ||
| const appKey = input.appKeyHeader; | ||
| if (!token || !serverId || !appKey) { | ||
| return { ok: false, status: 401, error: "missing_oauth" }; | ||
| } | ||
| const introspection = await introspectShadowServerAppToken({ | ||
| token, | ||
| serverId, | ||
| appKey, | ||
| shadowBaseUrl: input.shadowBaseUrl, | ||
| fetchImpl: input.fetchImpl | ||
| }).catch(() => null); | ||
| const context = introspection?.shadow; | ||
| if (!context) return { ok: false, status: 401, error: "invalid_token" }; | ||
| if (context.command !== input.expectedCommand) { | ||
| return { ok: false, status: 403, error: "wrong_command" }; | ||
| } | ||
| let commandInput; | ||
| if (input.requestInput !== void 0) { | ||
| commandInput = input.requestInput; | ||
| } else { | ||
| let body; | ||
| try { | ||
| body = JSON.parse(input.requestBody ?? "{}"); | ||
| } catch { | ||
| return { ok: false, status: 400, error: "invalid_json" }; | ||
| } | ||
| commandInput = body.input ?? {}; | ||
| } | ||
| return { | ||
| ok: true, | ||
| envelope: { | ||
| input: commandInput, | ||
| context | ||
| } | ||
| }; | ||
| } | ||
| function validateShadowServerAppJsonSchema(schema, value) { | ||
| if (!schema) return { ok: true }; | ||
| const issues = []; | ||
| validateJsonSchemaValue(schema, value, "", issues); | ||
| return issues.length ? { ok: false, issues } : { ok: true }; | ||
| } | ||
| function shadowServerAppActorDisplayName(envelopeOrContext) { | ||
| const context = "context" in envelopeOrContext ? envelopeOrContext.context : envelopeOrContext; | ||
| const actor = context.actor; | ||
| const profile = actor.profile; | ||
| return profile?.displayName?.trim() || profile?.username?.trim() || (actor.buddyAgentId ? `Buddy ${actor.buddyAgentId.slice(0, 8)}` : null) || (actor.userId ? `${actor.kind}:${actor.userId.slice(0, 8)}` : null) || `${actor.kind}:unknown`; | ||
| } | ||
| function shadowServerAppActorAvatarUrl(envelopeOrContext) { | ||
| const context = "context" in envelopeOrContext ? envelopeOrContext.context : envelopeOrContext; | ||
| return context.actor.profile?.avatarUrl ?? null; | ||
| } | ||
| function shadowServerAppActorRef(envelopeOrContext) { | ||
| const context = "context" in envelopeOrContext ? envelopeOrContext.context : envelopeOrContext; | ||
| const actor = context.actor; | ||
| return { | ||
| kind: actor.kind, | ||
| id: actor.buddyAgentId ?? actor.userId ?? actor.ownerId ?? "unknown", | ||
| userId: actor.userId ?? null, | ||
| buddyAgentId: actor.buddyAgentId ?? null, | ||
| ownerId: actor.ownerId ?? null, | ||
| displayName: shadowServerAppActorDisplayName(context), | ||
| avatarUrl: shadowServerAppActorAvatarUrl(context) | ||
| }; | ||
| } | ||
| function parseErrorResult(error) { | ||
| return failureResult(error.status, error.error, error.issues); | ||
| } | ||
| function failureResult(status, error, issues) { | ||
| return { | ||
| ok: false, | ||
| status, | ||
| body: issues === void 0 ? { ok: false, error } : { ok: false, error, issues } | ||
| }; | ||
| } | ||
| function validateJsonSchemaValue(schema, value, path, issues) { | ||
| if (Array.isArray(schema.oneOf)) { | ||
| const matches = schema.oneOf.some((option) => { | ||
| const nestedIssues = []; | ||
| if (option && typeof option === "object" && !Array.isArray(option)) { | ||
| validateJsonSchemaValue( | ||
| option, | ||
| value, | ||
| path, | ||
| nestedIssues | ||
| ); | ||
| } | ||
| return nestedIssues.length === 0; | ||
| }); | ||
| if (!matches) issues.push({ path, message: "Expected value matching one schema option" }); | ||
| return; | ||
| } | ||
| const enumValues = schema.enum; | ||
| if (Array.isArray(enumValues) && !enumValues.includes(value)) { | ||
| issues.push({ path, message: `Expected one of ${enumValues.map(String).join(", ")}` }); | ||
| return; | ||
| } | ||
| const type = schema.type; | ||
| if (type === "object") { | ||
| if (!value || typeof value !== "object" || Array.isArray(value)) { | ||
| issues.push({ path, message: "Expected object" }); | ||
| return; | ||
| } | ||
| const record = value; | ||
| const properties = schema.properties && typeof schema.properties === "object" && !Array.isArray(schema.properties) ? schema.properties : {}; | ||
| const required = Array.isArray(schema.required) ? schema.required.map(String) : []; | ||
| for (const key of required) { | ||
| if (!(key in record)) issues.push({ path: joinJsonPath(path, key), message: "Required" }); | ||
| } | ||
| for (const [key, propertySchema] of Object.entries(properties)) { | ||
| if (record[key] !== void 0) { | ||
| validateJsonSchemaValue(propertySchema, record[key], joinJsonPath(path, key), issues); | ||
| } | ||
| } | ||
| const additionalProperties = schema.additionalProperties && typeof schema.additionalProperties === "object" && !Array.isArray(schema.additionalProperties) ? schema.additionalProperties : null; | ||
| if (additionalProperties) { | ||
| for (const [key, nestedValue] of Object.entries(record)) { | ||
| if (!(key in properties)) { | ||
| validateJsonSchemaValue( | ||
| additionalProperties, | ||
| nestedValue, | ||
| joinJsonPath(path, key), | ||
| issues | ||
| ); | ||
| } | ||
| } | ||
| } else if (schema.additionalProperties === false) { | ||
| for (const key of Object.keys(record)) { | ||
| if (!(key in properties)) { | ||
| issues.push({ path: joinJsonPath(path, key), message: "Unknown property" }); | ||
| } | ||
| } | ||
| } | ||
| return; | ||
| } | ||
| if (type === "array") { | ||
| if (!Array.isArray(value)) { | ||
| issues.push({ path, message: "Expected array" }); | ||
| return; | ||
| } | ||
| const maxItems = typeof schema.maxItems === "number" ? schema.maxItems : null; | ||
| if (maxItems !== null && value.length > maxItems) { | ||
| issues.push({ path, message: `Expected at most ${maxItems} items` }); | ||
| } | ||
| const itemSchema = schema.items && typeof schema.items === "object" && !Array.isArray(schema.items) ? schema.items : null; | ||
| if (itemSchema) { | ||
| value.forEach( | ||
| (item, index) => validateJsonSchemaValue(itemSchema, item, `${path}[${index}]`, issues) | ||
| ); | ||
| } | ||
| return; | ||
| } | ||
| if (type === "string") { | ||
| if (typeof value !== "string") { | ||
| issues.push({ path, message: "Expected string" }); | ||
| return; | ||
| } | ||
| const maxLength = typeof schema.maxLength === "number" ? schema.maxLength : null; | ||
| const minLength = typeof schema.minLength === "number" ? schema.minLength : null; | ||
| if (minLength !== null && value.length < minLength) { | ||
| issues.push({ path, message: `Expected at least ${minLength} characters` }); | ||
| } | ||
| if (maxLength !== null && value.length > maxLength) { | ||
| issues.push({ path, message: `Expected at most ${maxLength} characters` }); | ||
| } | ||
| return; | ||
| } | ||
| if (type === "number" || type === "integer") { | ||
| if (typeof value !== "number" || !Number.isFinite(value)) { | ||
| issues.push({ path, message: "Expected number" }); | ||
| return; | ||
| } | ||
| if (type === "integer" && !Number.isInteger(value)) { | ||
| issues.push({ path, message: "Expected integer" }); | ||
| } | ||
| return; | ||
| } | ||
| if (type === "boolean" && typeof value !== "boolean") { | ||
| issues.push({ path, message: "Expected boolean" }); | ||
| } | ||
| } | ||
| function joinJsonPath(parent, key) { | ||
| return parent ? `${parent}.${key}` : key; | ||
| } | ||
| export { | ||
| SHADOW_SERVER_APP_PROTOCOL, | ||
| SHADOW_SERVER_APP_COMMAND_COMPLETED_EVENT, | ||
| SHADOW_SERVER_APP_COMMAND_FAILED_EVENT, | ||
| SHADOW_SERVER_APP_COMMAND_EVENTS, | ||
| ShadowServerAppHttpError, | ||
| shadowServerAppInboxTaskEndpoint, | ||
| buildShadowServerAppInboxTaskRequest, | ||
| getShadowServerAppTaskCardId, | ||
| buildShadowServerAppInboxDelivery, | ||
| getShadowServerAppInboxDeliveries, | ||
| getShadowServerAppInboxErrors, | ||
| getShadowServerAppPendingInboxTasks, | ||
| getShadowServerAppChannelMessageDeliveries, | ||
| getShadowServerAppChannelMessageErrors, | ||
| getShadowServerAppPendingChannelMessages, | ||
| hasShadowServerAppPendingOutbox, | ||
| unwrapShadowServerAppCommandPayload, | ||
| readShadowServerAppCommandResponse, | ||
| ShadowServerAppOutbox, | ||
| extractShadowServerAppBearerToken, | ||
| decodeShadowServerAppLaunchTokenHint, | ||
| fetchShadowServerAppLaunchInboxes, | ||
| deliverShadowServerAppLaunchOutbox, | ||
| normalizeShadowServerAppCommandInput, | ||
| createShadowServerAppManifest, | ||
| defineShadowServerApp, | ||
| createShadowServerAppRuntime, | ||
| ShadowServerAppCommandError, | ||
| shadowServerAppError, | ||
| ShadowServerAppRuntime, | ||
| introspectShadowServerAppToken, | ||
| parseShadowServerAppCommandRequest, | ||
| validateShadowServerAppJsonSchema, | ||
| shadowServerAppActorDisplayName, | ||
| shadowServerAppActorAvatarUrl, | ||
| shadowServerAppActorRef, | ||
| BUDDY_INBOX_DELIVERY_PERMISSION | ||
| }; |
| import { | ||
| BUDDY_INBOX_DELIVERY_PERMISSION, | ||
| decodeShadowServerAppLaunchTokenHint, | ||
| deliverShadowServerAppLaunchOutbox, | ||
| getShadowServerAppChannelMessageDeliveries, | ||
| getShadowServerAppChannelMessageErrors, | ||
| getShadowServerAppInboxDeliveries, | ||
| getShadowServerAppInboxErrors, | ||
| hasShadowServerAppPendingOutbox, | ||
| readShadowServerAppCommandResponse, | ||
| unwrapShadowServerAppCommandPayload | ||
| } from "./chunk-5ODYWMBC.js"; | ||
| // src/bridge.ts | ||
| var SHADOW_BRIDGE_CAPABILITIES = [ | ||
| "copilot.open", | ||
| "workspace.open", | ||
| "buddy.create.open", | ||
| "buddy.inboxes.list", | ||
| "buddy.grant.ensure", | ||
| "oauth.authorize", | ||
| "route.navigate" | ||
| ]; | ||
| function commandPath(basePath, commandName) { | ||
| return `${basePath.replace(/\/+$/u, "")}/${encodeURIComponent(commandName)}`; | ||
| } | ||
| function shadowServerAppMountedPathPrefix(windowRef) { | ||
| const win = windowRef ?? (typeof window === "undefined" ? null : window); | ||
| const pathname = win?.location?.pathname ?? ""; | ||
| const segments = pathname.split("/").filter(Boolean); | ||
| const shadowIndex = segments.indexOf("shadow"); | ||
| if (shadowIndex <= 0 || segments[shadowIndex + 1] !== "server") return ""; | ||
| return `/${segments.slice(0, shadowIndex).join("/")}`; | ||
| } | ||
| function shadowServerAppMountedPath(path, windowRef) { | ||
| const normalized = path.startsWith("/") ? path : `/${path}`; | ||
| return `${shadowServerAppMountedPathPrefix(windowRef)}${normalized}`; | ||
| } | ||
| function isRecord(value) { | ||
| return !!value && typeof value === "object" && !Array.isArray(value); | ||
| } | ||
| function withoutUndefined(value) { | ||
| if (value === void 0) return {}; | ||
| if (Array.isArray(value)) return value.map(withoutUndefined); | ||
| if (!isRecord(value)) return value; | ||
| return Object.fromEntries( | ||
| Object.entries(value).filter(([, entry]) => entry !== void 0).map(([key, entry]) => [key, withoutUndefined(entry)]) | ||
| ); | ||
| } | ||
| var ShadowServerAppBrowserClient = class { | ||
| bridge; | ||
| commandBasePath; | ||
| inboxesPath; | ||
| shadowApiBaseUrl; | ||
| fetchFn; | ||
| deliverLaunchOutboxFromBrowser; | ||
| win; | ||
| constructor(options = {}) { | ||
| this.bridge = new ShadowBridge(options); | ||
| const windowRef = options.windowRef ?? (typeof window === "undefined" ? null : window); | ||
| this.commandBasePath = options.commandBasePath ?? shadowServerAppMountedPath("/api/local/commands", windowRef); | ||
| this.inboxesPath = options.inboxesPath ?? shadowServerAppMountedPath("/api/local/inboxes", windowRef); | ||
| this.shadowApiBaseUrl = options.shadowApiBaseUrl; | ||
| this.fetchFn = options.fetch; | ||
| this.deliverLaunchOutboxFromBrowser = options.deliverLaunchOutboxFromBrowser ?? false; | ||
| this.win = windowRef; | ||
| } | ||
| bridgeAvailable() { | ||
| return this.bridge.isAvailable(); | ||
| } | ||
| launchToken(param = "shadow_launch") { | ||
| if (!this.win) return null; | ||
| return new URLSearchParams(this.win.location.search).get(param); | ||
| } | ||
| launchHeaders(headers = {}, options = {}) { | ||
| const token = this.launchToken(options.launchTokenParam); | ||
| return token ? { ...headers, "X-Shadow-Launch-Token": token } : headers; | ||
| } | ||
| async command(commandName, input = {}) { | ||
| const response = await this.fetch(commandPath(this.commandBasePath, commandName), { | ||
| method: "POST", | ||
| headers: this.launchHeaders({ "Content-Type": "application/json" }), | ||
| body: JSON.stringify({ input: withoutUndefined(input) }) | ||
| }); | ||
| const result = await readShadowServerAppCommandResponse(response); | ||
| return this.deliverLaunchOutbox(commandName, result); | ||
| } | ||
| async listBuddyInboxes(options = {}) { | ||
| if (this.bridge.isAvailable()) { | ||
| try { | ||
| return await this.bridge.listBuddyInboxes({ refresh: options.refresh }); | ||
| } catch { | ||
| } | ||
| } | ||
| const response = await this.fetch(this.inboxesPath, { headers: this.launchHeaders() }); | ||
| if (!response.ok) { | ||
| if (options.emptyOnError) return { inboxes: [] }; | ||
| const message = await response.text().catch(() => ""); | ||
| throw new Error(message || `Buddy inbox lookup failed (${response.status})`); | ||
| } | ||
| return await response.json(); | ||
| } | ||
| async ensureBuddyTaskGrant(input) { | ||
| const buddyAgentId = input.agentId?.trim(); | ||
| if (!buddyAgentId || !this.bridge.isAvailable()) return { granted: false, skipped: true }; | ||
| return this.bridge.ensureBuddyGrant( | ||
| { | ||
| buddyAgentId, | ||
| permissions: input.permissions ?? [BUDDY_INBOX_DELIVERY_PERMISSION], | ||
| reason: input.reason | ||
| }, | ||
| { timeoutMs: input.timeoutMs ?? 6e4 } | ||
| ); | ||
| } | ||
| openBuddyCreator(input = {}, options = {}) { | ||
| if (!this.bridge.isAvailable()) return Promise.resolve({ opened: false, agent: null }); | ||
| return this.bridge.openBuddyCreator(input, options); | ||
| } | ||
| openCopilot(delivery, options = {}) { | ||
| return this.bridge.openCopilot(delivery, options); | ||
| } | ||
| openWorkspaceResource(input, options = {}) { | ||
| return this.bridge.openWorkspaceResource(input, options); | ||
| } | ||
| authorizeOAuth(input, options = {}) { | ||
| if (!this.bridge.isAvailable()) return Promise.resolve({ opened: false }); | ||
| return this.bridge.authorizeOAuth(input, options); | ||
| } | ||
| inboxDeliveries(payload) { | ||
| return this.bridge.inboxDeliveries(payload); | ||
| } | ||
| inboxErrors(payload) { | ||
| return this.bridge.inboxErrors(payload); | ||
| } | ||
| channelMessageDeliveries(payload) { | ||
| return this.bridge.channelMessageDeliveries(payload); | ||
| } | ||
| channelMessageErrors(payload) { | ||
| return this.bridge.channelMessageErrors(payload); | ||
| } | ||
| async deliverLaunchOutbox(commandName, result) { | ||
| if (!this.deliverLaunchOutboxFromBrowser || !hasShadowServerAppPendingOutbox(result)) { | ||
| return result; | ||
| } | ||
| const launchToken = this.launchToken(); | ||
| if (!decodeShadowServerAppLaunchTokenHint(launchToken)) return result; | ||
| return await deliverShadowServerAppLaunchOutbox({ | ||
| commandName, | ||
| result, | ||
| launchToken, | ||
| shadowApiBaseUrl: this.shadowApiBaseUrl, | ||
| fetch: this.fetch.bind(this) | ||
| }); | ||
| } | ||
| fetch(input, init) { | ||
| if (this.fetchFn) return this.fetchFn(input, init); | ||
| return globalThis.fetch(input, init); | ||
| } | ||
| }; | ||
| function createShadowServerAppClient(options = {}) { | ||
| return new ShadowServerAppBrowserClient(options); | ||
| } | ||
| var createShadowServerAppBrowserClient = createShadowServerAppClient; | ||
| function createShadowServerAppRuntimeClient(options = {}) { | ||
| const windowRef = options.windowRef ?? (typeof window === "undefined" ? void 0 : window); | ||
| return new ShadowServerAppBrowserClient({ | ||
| ...options, | ||
| windowRef, | ||
| commandBasePath: options.commandBasePath ?? shadowServerAppMountedPath("/api/runtime/commands", windowRef), | ||
| inboxesPath: options.inboxesPath ?? shadowServerAppMountedPath("/api/runtime/inboxes", windowRef), | ||
| deliverLaunchOutboxFromBrowser: false | ||
| }); | ||
| } | ||
| var ShadowBridge = class _ShadowBridge { | ||
| static capabilitiesRequestType = "shadow.app.capabilities.request"; | ||
| static capabilitiesResponseType = "shadow.app.capabilities.response"; | ||
| static openCopilotRequestType = "shadow.app.copilot.open.request"; | ||
| static openCopilotResponseType = "shadow.app.copilot.open.response"; | ||
| static openWorkspaceResourceRequestType = "shadow.app.workspace.open.request"; | ||
| static openWorkspaceResourceResponseType = "shadow.app.workspace.open.response"; | ||
| static openBuddyCreatorRequestType = "shadow.app.buddy.create.request"; | ||
| static openBuddyCreatorResponseType = "shadow.app.buddy.create.response"; | ||
| static listBuddyInboxesRequestType = "shadow.app.buddy.inboxes.request"; | ||
| static listBuddyInboxesResponseType = "shadow.app.buddy.inboxes.response"; | ||
| static ensureBuddyGrantRequestType = "shadow.app.buddy.grant.request"; | ||
| static ensureBuddyGrantResponseType = "shadow.app.buddy.grant.response"; | ||
| static authorizeOAuthRequestType = "shadow.app.oauth.authorize.request"; | ||
| static authorizeOAuthResponseType = "shadow.app.oauth.authorize.response"; | ||
| static inboxDeliveries(payload) { | ||
| return getShadowServerAppInboxDeliveries(payload); | ||
| } | ||
| static inboxErrors(payload) { | ||
| return getShadowServerAppInboxErrors(payload); | ||
| } | ||
| static channelMessageDeliveries(payload) { | ||
| return getShadowServerAppChannelMessageDeliveries(payload); | ||
| } | ||
| static channelMessageErrors(payload) { | ||
| return getShadowServerAppChannelMessageErrors(payload); | ||
| } | ||
| static unwrapCommandPayload(payload) { | ||
| return unwrapShadowServerAppCommandPayload(payload); | ||
| } | ||
| appKey; | ||
| targetOrigin; | ||
| timeoutMs; | ||
| win; | ||
| hasLaunchContext; | ||
| pending = /* @__PURE__ */ new Map(); | ||
| onMessage = (event) => { | ||
| let data = event.data; | ||
| if (typeof data === "string") { | ||
| try { | ||
| data = JSON.parse(data || "{}"); | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
| if (!data || typeof data !== "object") return; | ||
| const record = data; | ||
| if (typeof record.requestId !== "string" || typeof record.type !== "string") return; | ||
| const entry = this.pending.get(record.requestId); | ||
| if (!entry || record.type !== entry.responseType) return; | ||
| this.pending.delete(record.requestId); | ||
| if (record.ok) entry.resolve(record.result); | ||
| else | ||
| entry.reject( | ||
| new Error(typeof record.error === "string" ? record.error : "Bridge request failed") | ||
| ); | ||
| }; | ||
| constructor(options = {}) { | ||
| this.win = options.windowRef ?? (typeof window === "undefined" ? null : window); | ||
| this.appKey = options.appKey ?? this.resolveLaunchAppKey(); | ||
| this.targetOrigin = options.targetOrigin ?? "*"; | ||
| this.timeoutMs = options.timeoutMs ?? 6e4; | ||
| this.hasLaunchContext = this.resolveLaunchContext(); | ||
| this.win?.addEventListener("message", this.onMessage); | ||
| } | ||
| dispose() { | ||
| this.win?.removeEventListener("message", this.onMessage); | ||
| for (const entry of this.pending.values()) { | ||
| entry.reject(new Error("ShadowBridge disposed")); | ||
| } | ||
| this.pending.clear(); | ||
| } | ||
| isAvailable() { | ||
| if (!this.win) return false; | ||
| return this.hasLaunchContext && (this.win.parent !== this.win || !!this.win.ReactNativeWebView); | ||
| } | ||
| capabilities(options = {}) { | ||
| return this.request( | ||
| _ShadowBridge.capabilitiesRequestType, | ||
| _ShadowBridge.capabilitiesResponseType, | ||
| {}, | ||
| options.timeoutMs ?? 15e3 | ||
| ); | ||
| } | ||
| openCopilot(deliveryOrInput, options = {}) { | ||
| const input = "delivery" in deliveryOrInput ? deliveryOrInput : { delivery: deliveryOrInput }; | ||
| return this.request( | ||
| _ShadowBridge.openCopilotRequestType, | ||
| _ShadowBridge.openCopilotResponseType, | ||
| input, | ||
| options.timeoutMs ?? 15e3 | ||
| ); | ||
| } | ||
| openWorkspaceResource(input, options = {}) { | ||
| return this.request( | ||
| _ShadowBridge.openWorkspaceResourceRequestType, | ||
| _ShadowBridge.openWorkspaceResourceResponseType, | ||
| input, | ||
| options.timeoutMs ?? 15e3 | ||
| ); | ||
| } | ||
| openBuddyCreator(input = {}, options = {}) { | ||
| return this.request( | ||
| _ShadowBridge.openBuddyCreatorRequestType, | ||
| _ShadowBridge.openBuddyCreatorResponseType, | ||
| input, | ||
| options.timeoutMs ?? 10 * 60 * 1e3 | ||
| ); | ||
| } | ||
| listBuddyInboxes(input = {}, options = {}) { | ||
| return this.request( | ||
| _ShadowBridge.listBuddyInboxesRequestType, | ||
| _ShadowBridge.listBuddyInboxesResponseType, | ||
| input, | ||
| options.timeoutMs ?? 15e3 | ||
| ); | ||
| } | ||
| ensureBuddyGrant(input, options = {}) { | ||
| return this.request( | ||
| _ShadowBridge.ensureBuddyGrantRequestType, | ||
| _ShadowBridge.ensureBuddyGrantResponseType, | ||
| input, | ||
| options.timeoutMs ?? 3e4 | ||
| ); | ||
| } | ||
| authorizeOAuth(input, options = {}) { | ||
| const payload = typeof input === "string" ? { authorizeUrl: input } : input; | ||
| return this.request( | ||
| _ShadowBridge.authorizeOAuthRequestType, | ||
| _ShadowBridge.authorizeOAuthResponseType, | ||
| payload, | ||
| options.timeoutMs ?? 10 * 60 * 1e3 | ||
| ); | ||
| } | ||
| unwrapCommandPayload(payload) { | ||
| return unwrapShadowServerAppCommandPayload(payload); | ||
| } | ||
| inboxDeliveries(payload) { | ||
| return getShadowServerAppInboxDeliveries(payload); | ||
| } | ||
| inboxErrors(payload) { | ||
| return getShadowServerAppInboxErrors(payload); | ||
| } | ||
| channelMessageDeliveries(payload) { | ||
| return getShadowServerAppChannelMessageDeliveries(payload); | ||
| } | ||
| channelMessageErrors(payload) { | ||
| return getShadowServerAppChannelMessageErrors(payload); | ||
| } | ||
| request(requestType, responseType, payload, timeoutMs = this.timeoutMs) { | ||
| if (!this.isAvailable()) { | ||
| return Promise.reject( | ||
| new Error("ShadowBridge is not available outside a Shadow launch frame") | ||
| ); | ||
| } | ||
| const requestId = `req_${Math.random().toString(36).slice(2)}`; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(requestId, { | ||
| responseType, | ||
| resolve, | ||
| reject | ||
| }); | ||
| this.postMessage({ | ||
| type: requestType, | ||
| requestId, | ||
| ...this.appKey ? { appKey: this.appKey } : {}, | ||
| ...payload | ||
| }); | ||
| this.win?.setTimeout(() => { | ||
| if (!this.pending.has(requestId)) return; | ||
| this.pending.delete(requestId); | ||
| reject(new Error("Bridge request timed out")); | ||
| }, timeoutMs); | ||
| }); | ||
| } | ||
| postMessage(message) { | ||
| if (!this.win) return; | ||
| if (this.win.ReactNativeWebView) { | ||
| this.win.ReactNativeWebView.postMessage(JSON.stringify(message)); | ||
| return; | ||
| } | ||
| this.win.parent.postMessage(message, this.targetOrigin); | ||
| } | ||
| resolveLaunchContext() { | ||
| if (!this.win) return false; | ||
| const storageKey = this.appKey ? `shadow.bridge.launch:${this.appKey}` : null; | ||
| const memoryContexts = this.win.__shadowBridgeLaunchContexts ??= {}; | ||
| const hasLaunchToken = new URLSearchParams(this.win.location.search).has("shadow_launch"); | ||
| if (hasLaunchToken) { | ||
| if (this.appKey) memoryContexts[this.appKey] = true; | ||
| try { | ||
| if (storageKey) this.win.sessionStorage?.setItem(storageKey, "1"); | ||
| } catch { | ||
| } | ||
| return true; | ||
| } | ||
| if (this.appKey && memoryContexts[this.appKey]) return true; | ||
| try { | ||
| return storageKey ? this.win.sessionStorage?.getItem(storageKey) === "1" : false; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| resolveLaunchAppKey() { | ||
| if (!this.win) return void 0; | ||
| const token = new URLSearchParams(this.win.location.search).get("shadow_launch"); | ||
| return decodeShadowServerAppLaunchTokenHint(token)?.appKey; | ||
| } | ||
| }; | ||
| export { | ||
| SHADOW_BRIDGE_CAPABILITIES, | ||
| shadowServerAppMountedPathPrefix, | ||
| shadowServerAppMountedPath, | ||
| ShadowServerAppBrowserClient, | ||
| createShadowServerAppClient, | ||
| createShadowServerAppBrowserClient, | ||
| createShadowServerAppRuntimeClient, | ||
| ShadowBridge | ||
| }; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
752485
0.02%14201
0.08%4
-20%+ Added
- Removed
Updated