@fedify/botkit 0.5.0-dev.210 → 0.5.0-dev.226
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/bot-group.test.d.ts +2 -0
- package/dist/bot-group.test.js +220 -0
- package/dist/bot-group.test.js.map +1 -0
- package/dist/bot-impl.d.ts +132 -13
- package/dist/bot-impl.d.ts.map +1 -1
- package/dist/bot-impl.js +400 -178
- package/dist/bot-impl.js.map +1 -1
- package/dist/bot-impl.test.js +214 -76
- package/dist/bot-impl.test.js.map +1 -1
- package/dist/bot.d.ts +94 -48
- package/dist/bot.d.ts.map +1 -1
- package/dist/bot.js +2 -104
- package/dist/bot.js.map +1 -1
- package/dist/bot.test.js +59 -0
- package/dist/bot.test.js.map +1 -1
- package/dist/components/FollowButton.d.ts +3 -1
- package/dist/components/FollowButton.d.ts.map +1 -1
- package/dist/components/FollowButton.js +2 -2
- package/dist/components/FollowButton.js.map +1 -1
- package/dist/components/Layout.js +1 -1
- package/dist/components/Layout.js.map +1 -1
- package/dist/components/Message.d.ts +2 -2
- package/dist/deno.js +3 -13
- package/dist/deno.js.map +1 -1
- package/dist/follow-impl.test.js +3 -3
- package/dist/follow-impl.test.js.map +1 -1
- package/dist/instance-impl.d.ts +158 -0
- package/dist/instance-impl.d.ts.map +1 -0
- package/dist/instance-impl.js +603 -0
- package/dist/instance-impl.js.map +1 -0
- package/dist/instance-impl.test.d.ts +2 -0
- package/dist/instance-impl.test.js +103 -0
- package/dist/instance-impl.test.js.map +1 -0
- package/dist/instance-multi.test.d.ts +2 -0
- package/dist/instance-multi.test.js +151 -0
- package/dist/instance-multi.test.js.map +1 -0
- package/dist/instance-routing.test.d.ts +2 -0
- package/dist/instance-routing.test.js +367 -0
- package/dist/instance-routing.test.js.map +1 -0
- package/dist/instance.d.ts +318 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +51 -0
- package/dist/instance.js.map +1 -0
- package/dist/message-impl.d.ts.map +1 -1
- package/dist/message-impl.js +17 -10
- package/dist/message-impl.js.map +1 -1
- package/dist/message-impl.test.js +43 -9
- package/dist/message-impl.test.js.map +1 -1
- package/dist/mod.d.ts +5 -3
- package/dist/mod.js +4 -2
- package/dist/pages.d.ts +10 -1
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +112 -41
- package/dist/pages.js.map +1 -1
- package/dist/pages.test.d.ts +2 -0
- package/dist/pages.test.js +170 -0
- package/dist/pages.test.js.map +1 -0
- package/dist/repository.d.ts +385 -138
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +595 -223
- package/dist/repository.js.map +1 -1
- package/dist/repository.test.js +564 -136
- package/dist/repository.test.js.map +1 -1
- package/dist/session-impl.d.ts.map +1 -1
- package/dist/session-impl.js +13 -4
- package/dist/session-impl.js.map +1 -1
- package/dist/session-impl.test.d.ts.map +1 -1
- package/dist/session-impl.test.js +9 -9
- package/dist/session-impl.test.js.map +1 -1
- package/dist/session.d.ts +8 -3
- package/dist/session.d.ts.map +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +27 -10
- package/dist/text.js.map +1 -1
- package/dist/text.test.js +37 -2
- package/dist/text.test.js.map +1 -1
- package/dist/uri.d.ts +46 -0
- package/dist/uri.d.ts.map +1 -0
- package/dist/uri.js +64 -0
- package/dist/uri.js.map +1 -0
- package/dist/uri.test.d.ts +2 -0
- package/dist/uri.test.js +93 -0
- package/dist/uri.test.js.map +1 -0
- package/package.json +6 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-impl.js","names":["#bots","#groups","#resolutionCache","options: InstanceImplOptions","metadata","#initialize","#dispatchInstanceActor","#dispatchInstanceActorKeyPairs","APEmoji","RawLike","bot: BotImpl<TContextData>","identifier: string","ctx: Context<TContextData>","resolved: BotImpl<TContextData> | null","identifierOrDispatcher: string | BotDispatcher<TContextData>","profileOrOptions?:\n | BotProfile<TContextData>\n | CreateBotGroupOptions<TContextData>","username: string","bot","_ctx: Context<TContextData>","activity: Activity","reason: { type: string; result?: unknown }","#resolveTargets","ctx: InboxContext<TContextData>","resolve: () =>\n | Promise<Iterable<string>>\n | Iterable<string>","bots: BotImpl<TContextData>[]","uri: URL | null","follow: Follow","undo: Undo","#localObjectTarget","accept: Accept","reject: Reject","like: RawLike","create: Create","uri: URL","announce: Announce","#firstBot","#instanceActorKeyPairs","values: { name: string }","name: string","data: CustomEmoji","url: URL","name: TEmojiName","session: Session<TContextData>","emojis: Readonly<Record<TEmojiName, CustomEmoji>>","request: Request","contextData: TContextData","response: Response","fileInfo: Awaited<ReturnType<typeof fs.stat>>","data: Uint8Array","origin: string | URL","id: string"],"sources":["../src/instance-impl.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2025–2026 Hong Minhee <https://hongminhee.org/>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\nimport {\n type Context,\n createFederation,\n type Federation,\n generateCryptoKeyPair,\n type InboxContext,\n type KvStore,\n type MessageQueue,\n type NodeInfo,\n type Software,\n} from \"@fedify/fedify\";\nimport {\n Accept,\n type Activity,\n type Actor,\n Announce,\n Application,\n Article,\n ChatMessage,\n Create,\n Delete,\n Emoji as APEmoji,\n Endpoints,\n Follow,\n Image,\n Like as RawLike,\n Link,\n Mention,\n Note,\n Question,\n Reject,\n Undo,\n} from \"@fedify/vocab\";\nimport { getLogger } from \"@logtape/logtape\";\nimport mimeDb from \"mime-db\";\nimport fs from \"node:fs/promises\";\nimport { getXForwardedRequest } from \"x-forwarded-fetch\";\nimport metadata from \"../deno.json\" with { type: \"json\" };\nimport { BotImpl } from \"./bot-impl.ts\";\nimport type { Bot, PagesOptions } from \"./bot.ts\";\nimport { BotGroupImpl, GroupBotImpl, wrapBotImpl } from \"./bot-impl.ts\";\nimport type { CustomEmoji, DeferredCustomEmoji } from \"./emoji.ts\";\nimport type {\n BotDispatcher,\n BotGroup,\n BotProfile,\n CreateBotGroupOptions,\n CreateInstanceOptions,\n Instance,\n} from \"./instance.ts\";\nimport { isMessageObject, isQuoteLink } from \"./message-impl.ts\";\nimport { app, multiApp } from \"./pages.tsx\";\nimport { KvRepository, type Repository } from \"./repository.ts\";\nimport type { Session } from \"./session.ts\";\nimport { parseLocalUri, rewriteLegacyObjectPath } from \"./uri.ts\";\n\n/**\n * The default identifier of the instance actor: an internal `Application`\n * actor that an {@link Instance} uses for signing shared-inbox related\n * requests on behalf of the whole instance. It can be overridden through\n * the {@link CreateInstanceOptions.instanceActorIdentifier} option; either\n * way, bots cannot take the effective identifier.\n * @since 0.5.0\n */\nexport const DEFAULT_INSTANCE_ACTOR_IDENTIFIER = \"__botkit_instance__\";\n\n/**\n * Options for creating an {@link InstanceImpl}.\n * @internal\n */\nexport interface InstanceImplOptions extends CreateInstanceOptions {\n collectionWindow?: number;\n\n /**\n * Whether the instance was created through the single-bot\n * `createBot()` compatibility path. A compatible instance keeps\n * the pre-0.5 behavior: the sole bot's key signs shared-inbox\n * requests and no instance actor is exposed.\n */\n compatMode?: boolean;\n}\n\n/**\n * The internal implementation of an {@link Instance}. It owns the single\n * Fedify {@link Federation} shared by every bot hosted on the instance, and\n * routes federation callbacks to the right bot by its identifier.\n * @internal\n */\nexport class InstanceImpl<TContextData>\n implements Omit<Instance<TContextData>, \"createBot\"> {\n readonly kv: KvStore;\n readonly queue?: MessageQueue;\n\n /**\n * The root repository shared by every bot hosted on the instance.\n */\n readonly repository: Repository;\n readonly software?: Software;\n readonly behindProxy: boolean;\n readonly pages: Required<PagesOptions>;\n readonly collectionWindow: number;\n readonly federation: Federation<TContextData>;\n readonly customEmojis: Record<string, CustomEmoji> = {};\n\n /**\n * The identifier of the bot actor that owns local objects whose URIs are\n * in the legacy (pre-0.5) format, which did not carry the identifier.\n */\n readonly legacyObjectUrisIdentifier?: string;\n\n /**\n * Whether the instance was created through the single-bot\n * `createBot()` compatibility path.\n */\n readonly compatMode: boolean;\n\n /**\n * The reserved identifier of the instance actor.\n */\n readonly instanceActorIdentifier: string;\n\n readonly #bots: Map<string, BotImpl<TContextData>> = new Map();\n readonly #groups: BotGroupImpl<TContextData>[] = [];\n\n /**\n * Memoizes dynamic bot resolution per Fedify context, which is stable\n * for the duration of one HTTP dispatch or one queue-delivered inbox\n * activity, so a dispatcher is invoked at most once per identifier per\n * request. Entries die with their context.\n */\n readonly #resolutionCache: WeakMap<\n Context<TContextData>,\n Map<string, BotImpl<TContextData> | null>\n > = new WeakMap();\n\n constructor(options: InstanceImplOptions) {\n this.kv = options.kv;\n this.queue = options.queue;\n this.repository = options.repository ?? new KvRepository(options.kv);\n this.software = options.software;\n this.behindProxy = options.behindProxy ?? false;\n this.pages = {\n color: \"green\",\n css: \"\",\n ...(options.pages ?? {}),\n };\n this.collectionWindow = options.collectionWindow ?? 50;\n this.legacyObjectUrisIdentifier = options.legacyObjectUris?.identifier;\n this.compatMode = options.compatMode ?? false;\n this.instanceActorIdentifier = options.instanceActorIdentifier ??\n DEFAULT_INSTANCE_ACTOR_IDENTIFIER;\n if (this.instanceActorIdentifier === \"\") {\n throw new TypeError(\"The instance actor identifier cannot be empty.\");\n }\n this.federation = createFederation<TContextData>({\n kv: options.kv,\n queue: options.queue,\n userAgent: {\n software: `BotKit/${metadata.version}`,\n },\n });\n this.#initialize();\n }\n\n #initialize(): void {\n this.federation\n .setActorDispatcher(\n \"/ap/actor/{identifier}\",\n async (ctx, identifier) => {\n if (\n !this.compatMode && identifier === this.instanceActorIdentifier\n ) {\n return await this.#dispatchInstanceActor(ctx);\n }\n const bot = await this.resolveBot(ctx, identifier);\n return await bot?.dispatchActor(ctx, identifier) ?? null;\n },\n )\n .mapHandle((ctx, username) => this.mapHandle(ctx, username))\n .setKeyPairsDispatcher(async (ctx, identifier) => {\n if (\n !this.compatMode && identifier === this.instanceActorIdentifier\n ) {\n return await this.#dispatchInstanceActorKeyPairs();\n }\n const bot = await this.resolveBot(ctx, identifier);\n return await bot?.dispatchActorKeyPairs(ctx, identifier) ?? [];\n });\n this.federation\n .setFollowersDispatcher(\n \"/ap/actor/{identifier}/followers\",\n async (ctx, identifier, cursor) => {\n const bot = await this.resolveBot(ctx, identifier);\n return await bot?.dispatchFollowers(ctx, identifier, cursor) ??\n null;\n },\n )\n .setFirstCursor(async (ctx, identifier) => {\n const bot = await this.resolveBot(ctx, identifier);\n return bot?.getFollowersFirstCursor(ctx, identifier) ?? null;\n })\n .setCounter(async (ctx, identifier) => {\n const bot = await this.resolveBot(ctx, identifier);\n return await bot?.countFollowers(ctx, identifier) ?? null;\n });\n this.federation\n .setOutboxDispatcher(\n \"/ap/actor/{identifier}/outbox\",\n async (ctx, identifier, cursor) => {\n const bot = await this.resolveBot(ctx, identifier);\n return await bot?.dispatchOutbox(ctx, identifier, cursor) ?? null;\n },\n )\n .setFirstCursor(async (ctx, identifier) => {\n const bot = await this.resolveBot(ctx, identifier);\n return bot?.getOutboxFirstCursor(ctx, identifier) ?? null;\n })\n .setCounter(async (ctx, identifier) => {\n const bot = await this.resolveBot(ctx, identifier);\n return await bot?.countOutbox(ctx, identifier) ?? null;\n });\n this.federation\n .setObjectDispatcher(\n Follow,\n \"/ap/actor/{identifier}/follow/{id}\",\n async (ctx, values) => {\n const bot = await this.resolveBot(ctx, values.identifier);\n return await bot?.dispatchFollow(ctx, values) ?? null;\n },\n )\n .authorize(async (ctx, values) => {\n const bot = await this.resolveBot(ctx, values.identifier);\n return await bot?.authorizeFollow(ctx, values) ?? false;\n });\n this.federation.setObjectDispatcher(\n Create,\n \"/ap/actor/{identifier}/create/{id}\",\n async (ctx, values) => {\n const bot = await this.resolveBot(ctx, values.identifier);\n return await bot?.dispatchCreate(ctx, values) ?? null;\n },\n );\n this.federation.setObjectDispatcher(\n Article,\n \"/ap/actor/{identifier}/article/{id}\",\n async (ctx, values) => {\n const bot = await this.resolveBot(ctx, values.identifier);\n return await bot?.dispatchMessage(Article, ctx, values.id) ?? null;\n },\n );\n this.federation.setObjectDispatcher(\n ChatMessage,\n \"/ap/actor/{identifier}/chat-message/{id}\",\n async (ctx, values) => {\n const bot = await this.resolveBot(ctx, values.identifier);\n return await bot?.dispatchMessage(ChatMessage, ctx, values.id) ??\n null;\n },\n );\n this.federation.setObjectDispatcher(\n Note,\n \"/ap/actor/{identifier}/note/{id}\",\n async (ctx, values) => {\n const bot = await this.resolveBot(ctx, values.identifier);\n return await bot?.dispatchMessage(Note, ctx, values.id) ?? null;\n },\n );\n this.federation.setObjectDispatcher(\n Question,\n \"/ap/actor/{identifier}/question/{id}\",\n async (ctx, values) => {\n const bot = await this.resolveBot(ctx, values.identifier);\n return await bot?.dispatchMessage(Question, ctx, values.id) ?? null;\n },\n );\n this.federation.setObjectDispatcher(\n Announce,\n \"/ap/actor/{identifier}/announce/{id}\",\n async (ctx, values) => {\n const bot = await this.resolveBot(ctx, values.identifier);\n return await bot?.dispatchAnnounce(ctx, values) ?? null;\n },\n );\n this.federation.setObjectDispatcher(\n APEmoji,\n \"/ap/emoji/{name}\",\n (ctx, values) => this.dispatchEmoji(ctx, values),\n );\n this.federation\n .setInboxListeners(\"/ap/actor/{identifier}/inbox\", \"/ap/inbox\")\n .onUnverifiedActivity((ctx, activity, reason) =>\n this.onUnverifiedActivity(ctx, activity, reason)\n )\n .on(Follow, (ctx, follow) => this.onFollowed(ctx, follow))\n .on(Undo, (ctx, undo) => this.onUndone(ctx, undo))\n .on(Accept, (ctx, accept) => this.onFollowAccepted(ctx, accept))\n .on(Reject, (ctx, reject) => this.onFollowRejected(ctx, reject))\n .on(Create, (ctx, create) => this.onCreated(ctx, create))\n .on(Announce, (ctx, announce) => this.onAnnounced(ctx, announce))\n .on(RawLike, (ctx, like) => this.onLiked(ctx, like))\n .setSharedKeyDispatcher((ctx) => this.dispatchSharedKey(ctx));\n if (this.software != null) {\n this.federation.setNodeInfoDispatcher(\n \"/nodeinfo/2.1\",\n (ctx) => this.dispatchNodeInfo(ctx),\n );\n }\n }\n\n /**\n * Registers a bot on the instance. Invoked by the {@link BotImpl}\n * constructor.\n * @param bot The bot to register.\n * @throws {TypeError} If a bot with the same identifier or username\n * already exists on the instance.\n */\n addBot(bot: BotImpl<TContextData>): void {\n if (\n !this.compatMode && bot.identifier === this.instanceActorIdentifier\n ) {\n throw new TypeError(\n `The identifier is reserved for the instance actor: ${bot.identifier}`,\n );\n }\n if (this.#bots.has(bot.identifier)) {\n throw new TypeError(\n `A bot with the identifier already exists: ${bot.identifier}`,\n );\n }\n // Fediverse usernames are looked up case-insensitively (WebFinger\n // acct: resources and mentions vary in casing), so two usernames\n // differing only in case would be indistinguishable:\n const username = bot.username.toLowerCase();\n for (const existing of this.#bots.values()) {\n if (existing.username.toLowerCase() === username) {\n throw new TypeError(\n `A bot with the username already exists: ${bot.username}`,\n );\n }\n }\n this.#bots.set(bot.identifier, bot);\n }\n\n /**\n * Resolves a bot hosted on the instance by its identifier.\n * @param identifier The identifier of the bot to resolve.\n * @returns The resolved bot, or `undefined` if no bot has the identifier.\n */\n getBot(identifier: string): BotImpl<TContextData> | undefined {\n return this.#bots.get(identifier);\n }\n\n /**\n * Resolves a bot by its identifier: bots registered statically win, then\n * the dynamic bot groups are probed in their registration order.\n * Dynamic resolutions are memoized per context.\n * @param ctx The Fedify context of the resolution.\n * @param identifier The identifier of the bot to resolve.\n * @returns The resolved bot, or `null` if no bot has the identifier.\n */\n async resolveBot(\n ctx: Context<TContextData>,\n identifier: string,\n ): Promise<BotImpl<TContextData> | null> {\n // The instance actor's identifier is reserved; no bot, static or\n // dynamic, resolves under it. (addBot() already rejects static bots\n // with the reserved identifier; this keeps the invariant local.)\n if (!this.compatMode && identifier === this.instanceActorIdentifier) {\n return null;\n }\n const staticBot = this.#bots.get(identifier);\n if (staticBot != null) return staticBot;\n if (this.#groups.length < 1) return null;\n let cache = this.#resolutionCache.get(ctx);\n if (cache == null) {\n cache = new Map();\n this.#resolutionCache.set(ctx, cache);\n }\n const cached = cache.get(identifier);\n if (cached !== undefined) return cached;\n let resolved: BotImpl<TContextData> | null = null;\n for (const group of this.#groups) {\n const profile = await group.dispatcher(ctx, identifier);\n if (profile != null) {\n resolved = new GroupBotImpl(group, identifier, profile);\n break;\n }\n }\n cache.set(identifier, resolved);\n return resolved;\n }\n\n /**\n * Every bot hosted on the instance.\n */\n get bots(): Iterable<BotImpl<TContextData>> {\n return this.#bots.values();\n }\n\n /**\n * The number of bots hosted on the instance.\n */\n get botCount(): number {\n return this.#bots.size;\n }\n\n #firstBot(): BotImpl<TContextData> | undefined {\n return this.#bots.values().next().value;\n }\n\n createBot(\n identifier: string,\n profile: BotProfile<TContextData>,\n ): Bot<TContextData>;\n createBot(\n dispatcher: BotDispatcher<TContextData>,\n options?: CreateBotGroupOptions<TContextData>,\n ): BotGroup<TContextData>;\n createBot(\n identifierOrDispatcher: string | BotDispatcher<TContextData>,\n profileOrOptions?:\n | BotProfile<TContextData>\n | CreateBotGroupOptions<TContextData>,\n ): Bot<TContextData> | BotGroup<TContextData> {\n if (typeof identifierOrDispatcher !== \"string\") {\n const group = new BotGroupImpl(\n this,\n identifierOrDispatcher,\n profileOrOptions as CreateBotGroupOptions<TContextData> | undefined,\n );\n this.#groups.push(group);\n return group;\n }\n const profile = profileOrOptions as BotProfile<TContextData> | undefined;\n if (profile == null || profile.username == null) {\n throw new TypeError(\"The bot profile with a username is required.\");\n }\n const bot = new BotImpl<TContextData>({\n instance: this,\n identifier: identifierOrDispatcher,\n kv: this.kv,\n class: profile.class,\n username: profile.username,\n name: profile.name,\n summary: profile.summary,\n icon: profile.icon,\n image: profile.image,\n properties: profile.properties,\n followerPolicy: profile.followerPolicy,\n });\n return wrapBotImpl(bot);\n }\n\n /**\n * Resolves a bot hosted on the instance by its username, including\n * dynamically resolved bots.\n * @param ctx The Fedify context of the resolution.\n * @param username The username of the bot to resolve.\n * @returns The resolved bot, or `null` if no bot has the username.\n */\n async resolveBotByUsername(\n ctx: Context<TContextData>,\n username: string,\n ): Promise<BotImpl<TContextData> | null> {\n const identifier = await this.mapHandle(ctx, username);\n if (identifier == null) return null;\n return await this.resolveBot(ctx, identifier);\n }\n\n async mapHandle(\n ctx: Context<TContextData>,\n username: string,\n ): Promise<string | null> {\n // WebFinger lookups and profile page visits vary in casing, so static\n // usernames are matched case-insensitively:\n const normalized = username.toLowerCase();\n for (const bot of this.#bots.values()) {\n if (bot.username.toLowerCase() === normalized) return bot.identifier;\n }\n for (const group of this.#groups) {\n if (group.mapUsername == null) continue;\n const identifier = await group.mapUsername(ctx, username);\n if (identifier == null) continue;\n // The mapping must resolve to a bot of the very group that mapped it;\n // otherwise a group could hijack usernames of other bots:\n const bot = await this.resolveBot(ctx, identifier);\n if (bot instanceof GroupBotImpl && bot.group === group) {\n return identifier;\n }\n }\n // Unless a group maps usernames explicitly, usernames are assumed to\n // equal identifiers. The fallback applies only to dynamic groups\n // without a mapUsername; a static bot's internal identifier is not\n // a public username:\n const bot = await this.resolveBot(ctx, username);\n if (bot instanceof GroupBotImpl && bot.group.mapUsername == null) {\n return username;\n }\n return null;\n }\n\n onUnverifiedActivity(\n _ctx: Context<TContextData>,\n activity: Activity,\n reason: { type: string; result?: unknown },\n ): Response | void {\n if (\n activity instanceof Delete &&\n reason.type === \"keyFetchError\" &&\n typeof reason.result === \"object\" && reason.result != null &&\n \"status\" in reason.result &&\n reason.result.status === 410\n ) {\n return new Response(null, { status: 202 });\n }\n }\n\n /**\n * Resolves which bots an incoming shared-inbox activity is relevant to.\n * On a compatible (single-bot) instance every hosted bot is returned,\n * preserving the pre-0.5 behavior. A personal-inbox delivery targets\n * its recipient only. Otherwise the given resolver computes the\n * relevant bot identifiers.\n */\n async #resolveTargets(\n ctx: InboxContext<TContextData>,\n resolve: () =>\n | Promise<Iterable<string>>\n | Iterable<string>,\n ): Promise<BotImpl<TContextData>[]> {\n if (this.compatMode) return [...this.#bots.values()];\n if (ctx.recipient != null) {\n const bot = await this.resolveBot(ctx, ctx.recipient);\n return bot == null ? [] : [bot];\n }\n const identifiers = new Set(await resolve());\n const bots: BotImpl<TContextData>[] = [];\n for (const identifier of identifiers) {\n const bot = await this.resolveBot(ctx, identifier);\n if (bot != null) bots.push(bot);\n }\n return bots;\n }\n\n /**\n * Attributes a local object URI to its owning bot identifier, or logs and\n * yields nothing when the URI is not a local object. Activities on\n * objects the instance does not own cannot be attributed to any bot on\n * a multi-bot instance, so they are dropped.\n */\n #localObjectTarget(\n ctx: InboxContext<TContextData>,\n uri: URL | null,\n ): Iterable<string> {\n const parsed = parseLocalUri(ctx, uri, this.legacyObjectUrisIdentifier);\n if (\n parsed?.type === \"object\" &&\n typeof parsed.values.identifier === \"string\"\n ) {\n return [parsed.values.identifier];\n }\n const logger = getLogger([\"botkit\", \"instance\", \"inbox\"]);\n logger.debug(\n \"The object {uri} is not owned by any bot on this instance; \" +\n \"the activity is not routed.\",\n { uri: uri?.href },\n );\n return [];\n }\n\n async onFollowed(\n ctx: InboxContext<TContextData>,\n follow: Follow,\n ): Promise<void> {\n const bots = await this.#resolveTargets(ctx, () => {\n const parsed = ctx.parseUri(follow.objectId);\n return parsed?.type === \"actor\" ? [parsed.identifier] : [];\n });\n for (const bot of bots) await bot.onFollowed(ctx, follow);\n }\n\n async onUndone(\n ctx: InboxContext<TContextData>,\n undo: Undo,\n ): Promise<void> {\n const object = await undo.getObject(ctx);\n if (object instanceof Follow) {\n const bots = await this.#resolveTargets(ctx, () => {\n const parsed = ctx.parseUri(object.objectId);\n return parsed?.type === \"actor\" ? [parsed.identifier] : [];\n });\n for (const bot of bots) await bot.onUnfollowed(ctx, undo);\n } else if (object instanceof RawLike) {\n const bots = await this.#resolveTargets(\n ctx,\n () => this.#localObjectTarget(ctx, object.objectId),\n );\n for (const bot of bots) await bot.onUnliked(ctx, undo);\n } else {\n const logger = getLogger([\"botkit\", \"bot\", \"inbox\"]);\n logger.warn(\n \"The Undo object {undoId} is not about Follow or Like: {object}.\",\n { undoId: undo.id?.href, object },\n );\n }\n }\n\n async onFollowAccepted(\n ctx: InboxContext<TContextData>,\n accept: Accept,\n ): Promise<void> {\n const bots = await this.#resolveTargets(\n ctx,\n () => this.#localObjectTarget(ctx, accept.objectId),\n );\n for (const bot of bots) await bot.onFollowAccepted(ctx, accept);\n }\n\n async onFollowRejected(\n ctx: InboxContext<TContextData>,\n reject: Reject,\n ): Promise<void> {\n const bots = await this.#resolveTargets(\n ctx,\n () => this.#localObjectTarget(ctx, reject.objectId),\n );\n for (const bot of bots) await bot.onFollowRejected(ctx, reject);\n }\n\n async onLiked(\n ctx: InboxContext<TContextData>,\n like: RawLike,\n ): Promise<void> {\n const bots = await this.#resolveTargets(\n ctx,\n () => this.#localObjectTarget(ctx, like.objectId),\n );\n for (const bot of bots) await bot.onLiked(ctx, like);\n }\n\n async onCreated(\n ctx: InboxContext<TContextData>,\n create: Create,\n ): Promise<void> {\n const bots = await this.#resolveTargets(ctx, async () => {\n const targets = new Set<string>();\n // Bots following the author see the message on their timeline:\n if (create.actorId != null) {\n for await (\n const identifier of this.repository.findFollowedBots(create.actorId)\n ) {\n targets.add(identifier);\n }\n }\n // Bots addressed directly, or whose followers collection is\n // addressed (either on the activity or on the embedded object):\n const addAddressee = (uri: URL) => {\n const parsed = ctx.parseUri(uri);\n if (parsed?.type === \"actor\" || parsed?.type === \"followers\") {\n if (parsed.identifier != null) targets.add(parsed.identifier);\n }\n };\n for (const uri of [...create.toIds, ...create.ccIds]) {\n addAddressee(uri);\n }\n const addLocalObject = (uri: URL | null) => {\n const parsed = parseLocalUri(\n ctx,\n uri,\n this.legacyObjectUrisIdentifier,\n );\n if (\n parsed?.type === \"object\" &&\n typeof parsed.values.identifier === \"string\"\n ) {\n targets.add(parsed.values.identifier);\n }\n };\n const object = await create.getObject(ctx);\n if (isMessageObject(object)) {\n for (const uri of [...object.toIds, ...object.ccIds]) {\n addAddressee(uri);\n }\n // Bots mentioned in or quoted by the message:\n for await (const tag of object.getTags(ctx)) {\n if (tag instanceof Mention && tag.href != null) {\n const parsed = ctx.parseUri(tag.href);\n if (parsed?.type === \"actor\") targets.add(parsed.identifier);\n } else if (tag instanceof Link && isQuoteLink(tag)) {\n addLocalObject(tag.href);\n }\n }\n addLocalObject(object.quoteUrl);\n // Bots whose message is replied to:\n addLocalObject(object.replyTargetId);\n }\n return targets;\n });\n for (const bot of bots) await bot.onCreated(ctx, create);\n }\n\n async onAnnounced(\n ctx: InboxContext<TContextData>,\n announce: Announce,\n ): Promise<void> {\n const bots = await this.#resolveTargets(ctx, async () => {\n const targets = new Set<string>();\n // Bots following the sharer see the share on their timeline:\n if (announce.actorId != null) {\n for await (\n const identifier of this.repository.findFollowedBots(\n announce.actorId,\n )\n ) {\n targets.add(identifier);\n }\n }\n // Bots addressed directly, or whose followers collection is\n // addressed, or whose message is shared:\n for (const uri of [...announce.toIds, ...announce.ccIds]) {\n const parsed = ctx.parseUri(uri);\n if (parsed?.type === \"actor\" || parsed?.type === \"followers\") {\n if (parsed.identifier != null) targets.add(parsed.identifier);\n }\n }\n const parsedObject = parseLocalUri(\n ctx,\n announce.objectId,\n this.legacyObjectUrisIdentifier,\n );\n if (\n parsedObject?.type === \"object\" &&\n typeof parsedObject.values.identifier === \"string\"\n ) {\n targets.add(parsedObject.values.identifier);\n }\n return targets;\n });\n for (const bot of bots) await bot.onAnnounced(ctx, announce);\n }\n\n dispatchSharedKey(_ctx: Context<TContextData>): { identifier: string } {\n if (this.compatMode) {\n const bot = this.#firstBot();\n if (bot == null) {\n throw new TypeError(\n \"The instance has no bots; the shared inbox key cannot be \" +\n \"dispatched.\",\n );\n }\n return { identifier: bot.identifier };\n }\n return { identifier: this.instanceActorIdentifier };\n }\n\n async #dispatchInstanceActor(\n ctx: Context<TContextData>,\n ): Promise<Actor> {\n const keyPairs = await ctx.getActorKeyPairs(this.instanceActorIdentifier);\n return new Application({\n id: ctx.getActorUri(this.instanceActorIdentifier),\n preferredUsername: this.instanceActorIdentifier,\n name: \"Instance actor\",\n summary: \"An internal actor the instance uses for signing requests \" +\n \"on behalf of the whole instance.\",\n inbox: ctx.getInboxUri(this.instanceActorIdentifier),\n endpoints: new Endpoints({\n sharedInbox: ctx.getInboxUri(),\n }),\n publicKey: keyPairs[0].cryptographicKey,\n assertionMethods: keyPairs.map((pair) => pair.multikey),\n discoverable: false,\n });\n }\n\n #instanceActorKeyPairs?: Promise<CryptoKeyPair[]>;\n\n #dispatchInstanceActorKeyPairs(): Promise<CryptoKeyPair[]> {\n // Concurrent cold-start requests must not generate competing key pairs\n // and overwrite each other, so the whole load-or-generate sequence is\n // memoized in process:\n if (this.#instanceActorKeyPairs != null) {\n return this.#instanceActorKeyPairs;\n }\n const promise = (async () => {\n let keyPairs = await this.repository.getKeyPairs(\n this.instanceActorIdentifier,\n );\n if (keyPairs == null) {\n const rsa = await generateCryptoKeyPair(\"RSASSA-PKCS1-v1_5\");\n const ed25519 = await generateCryptoKeyPair(\"Ed25519\");\n keyPairs = [rsa, ed25519];\n await this.repository.setKeyPairs(\n this.instanceActorIdentifier,\n keyPairs,\n );\n }\n return keyPairs;\n })();\n this.#instanceActorKeyPairs = promise;\n // A failed attempt (e.g. a transient storage error) must not be cached\n // forever:\n promise.catch(() => {\n if (this.#instanceActorKeyPairs === promise) {\n this.#instanceActorKeyPairs = undefined;\n }\n });\n return promise;\n }\n\n dispatchNodeInfo(_ctx: Context<TContextData>): NodeInfo {\n return {\n software: this.software!,\n protocols: [\"activitypub\"],\n services: {\n outbound: [\"atom1.0\"], // TODO\n },\n usage: {\n users: {\n total: this.botCount,\n activeMonth: this.botCount, // FIXME\n activeHalfyear: this.botCount, // FIXME\n },\n localPosts: 0, // FIXME\n localComments: 0,\n },\n };\n }\n\n dispatchEmoji(\n ctx: Context<TContextData>,\n values: { name: string },\n ): APEmoji | null {\n const customEmoji = this.customEmojis[values.name];\n if (customEmoji == null) return null;\n return this.getEmoji(ctx, values.name, customEmoji);\n }\n\n getEmoji(\n ctx: Context<TContextData>,\n name: string,\n data: CustomEmoji,\n ): APEmoji {\n let url: URL;\n if (\"url\" in data) {\n url = new URL(data.url);\n } else {\n // @ts-ignore: data.type satisfies keyof typeof mimeDb\n const t = mimeDb[data.type];\n url = new URL(\n `/emojis/${name}${\n t == null || t.extensions == null || t.extensions.length < 1\n ? \"\"\n : `.${t.extensions[0]}`\n }`,\n ctx.origin,\n );\n }\n return new APEmoji({\n id: ctx.getObjectUri(APEmoji, { name }),\n name: `:${name}:`,\n icon: new Image({\n mediaType: data.type,\n url,\n }),\n });\n }\n\n addCustomEmoji<TEmojiName extends string>(\n name: TEmojiName,\n data: CustomEmoji,\n ): DeferredCustomEmoji<TContextData> {\n if (!name.match(/^[a-z0-9-_]+$/i)) {\n throw new TypeError(\n `Invalid custom emoji name: ${name}. It must match /^[a-z0-9-_]+$/i.`,\n );\n } else if (name in this.customEmojis) {\n throw new TypeError(`Duplicate custom emoji name: ${name}`);\n } else if (!data.type.startsWith(\"image/\")) {\n throw new TypeError(`Unsupported media type: ${data.type}`);\n }\n this.customEmojis[name] = data;\n return (session: Session<TContextData>) =>\n this.getEmoji(\n session.context,\n name,\n data,\n );\n }\n\n addCustomEmojis<TEmojiName extends string>(\n emojis: Readonly<Record<TEmojiName, CustomEmoji>>,\n ): Readonly<Record<TEmojiName, DeferredCustomEmoji<TContextData>>> {\n const emojiMap = {} as Record<\n TEmojiName,\n DeferredCustomEmoji<TContextData>\n >;\n for (const name in emojis) {\n emojiMap[name] = this.addCustomEmoji(name, emojis[name]);\n }\n return emojiMap;\n }\n\n async addCollectionInverseProperty(\n request: Request,\n contextData: TContextData,\n response: Response,\n ): Promise<Response> {\n if (!response.ok) return response;\n const ctx = this.federation.createContext(request, contextData);\n const parsed = ctx.parseUri(new URL(request.url));\n if (\n parsed == null ||\n (parsed.type !== \"outbox\" && parsed.type !== \"followers\") ||\n parsed.identifier == null\n ) {\n return response;\n }\n const contentType = response.headers.get(\"Content-Type\");\n if (\n contentType == null ||\n (\n !contentType.startsWith(\"application/activity+json\") &&\n !contentType.startsWith(\"application/ld+json\")\n )\n ) {\n return response;\n }\n const body = await response.json();\n if (typeof body !== \"object\" || body == null || Array.isArray(body)) {\n return new Response(JSON.stringify(body), {\n headers: response.headers,\n status: response.status,\n statusText: response.statusText,\n });\n }\n const property = parsed.type === \"outbox\" ? \"outboxOf\" : \"followersOf\";\n const actorUri = ctx.getActorUri(parsed.identifier).href;\n if (body[property] === actorUri) {\n return new Response(JSON.stringify(body), {\n headers: response.headers,\n status: response.status,\n statusText: response.statusText,\n });\n }\n const headers = new Headers(response.headers);\n headers.delete(\"Content-Length\");\n return new Response(JSON.stringify({ ...body, [property]: actorUri }), {\n headers,\n status: response.status,\n statusText: response.statusText,\n });\n }\n\n async fetch(request: Request, contextData: TContextData): Promise<Response> {\n if (this.behindProxy) {\n request = await getXForwardedRequest(request);\n }\n const url = new URL(request.url);\n if (\n (request.method === \"GET\" || request.method === \"HEAD\") &&\n this.legacyObjectUrisIdentifier != null\n ) {\n // Dereferenceable requests to object URIs in the legacy (pre-0.5)\n // format are permanently redirected to their canonical URIs:\n const rewrittenPath = rewriteLegacyObjectPath(\n url.pathname,\n this.legacyObjectUrisIdentifier,\n );\n if (rewrittenPath != null) {\n const location = new URL(url.href);\n location.pathname = rewrittenPath;\n return new Response(null, {\n status: 301,\n headers: { Location: location.href },\n });\n }\n }\n if (\n url.pathname.startsWith(\"/.well-known/\") ||\n url.pathname.startsWith(\"/ap/\") ||\n url.pathname.startsWith(\"/nodeinfo/\")\n ) {\n const response = await this.federation.fetch(request, { contextData });\n return await this.addCollectionInverseProperty(\n request,\n contextData,\n response,\n );\n }\n const match = /^\\/emojis\\/([a-z0-9-_]+)(?:$|\\.)/.exec(url.pathname);\n if (match != null) {\n const customEmoji = this.customEmojis[match[1]];\n if (customEmoji == null || !(\"file\" in customEmoji)) {\n return new Response(\"Not Found\", { status: 404 });\n }\n // Custom emojis are small images, so reading them into memory avoids\n // managing a file handle whose readableWebStream() does not close it\n // on completion (leaking a descriptor per request):\n let fileInfo: Awaited<ReturnType<typeof fs.stat>>;\n let data: Uint8Array;\n try {\n fileInfo = await fs.stat(customEmoji.file);\n data = await fs.readFile(customEmoji.file);\n } catch (error) {\n if (\n typeof error === \"object\" && error != null && \"code\" in error &&\n error.code === \"ENOENT\"\n ) {\n return new Response(\"Not Found\", { status: 404 });\n }\n throw error;\n }\n const mtime = fileInfo.mtime ?? new Date();\n return new Response(data as Uint8Array<ArrayBuffer>, {\n headers: {\n \"Content-Type\": customEmoji.type,\n \"Content-Length\": fileInfo.size.toString(),\n \"Cache-Control\": \"public, max-age=31536000, immutable\",\n \"Last-Modified\": mtime.toUTCString(),\n \"ETag\": `\"${mtime.getTime().toString(36)}${\n fileInfo.size.toString(36)\n }\"`,\n },\n });\n }\n if (this.compatMode) {\n const bot = this.#firstBot();\n if (bot == null) return new Response(\"Not Found\", { status: 404 });\n return await app.fetch(request, { bot, contextData });\n }\n return await multiApp.fetch(request, {\n instance: this as InstanceImpl<unknown>,\n contextData,\n });\n }\n\n /**\n * The web page URL of a bot hosted on the instance. A compatible\n * (single-bot) instance serves its bot at the web root; a multi-bot\n * instance serves each bot under a path derived from its username.\n * @param bot The bot to get the web page URL of.\n * @param origin The origin of the URL.\n * @returns The web page URL of the bot.\n */\n getBotWebUrl(bot: BotImpl<TContextData>, origin: string | URL): URL {\n return new URL(\n this.compatMode ? \"/\" : `/@${encodeURIComponent(bot.username)}`,\n origin,\n );\n }\n\n /**\n * The web permalink of a message published by a bot hosted on\n * the instance.\n * @param bot The bot that published the message.\n * @param id The UUID of the message.\n * @param origin The origin of the URL.\n * @returns The web permalink of the message.\n */\n getMessageWebUrl(\n bot: BotImpl<TContextData>,\n id: string,\n origin: string | URL,\n ): URL {\n return new URL(\n this.compatMode\n ? `/message/${id}`\n : `/@${encodeURIComponent(bot.username)}/${id}`,\n origin,\n );\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
3
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
4
|
+
|
|
5
|
+
import { createInstance } from "./instance.js";
|
|
6
|
+
import { MemoryKvStore } from "@fedify/fedify/federation";
|
|
7
|
+
import assert from "node:assert";
|
|
8
|
+
import { describe, test } from "node:test";
|
|
9
|
+
|
|
10
|
+
//#region src/instance-impl.test.ts
|
|
11
|
+
describe("createInstance()", () => {
|
|
12
|
+
test("serves a static bot's actor", async () => {
|
|
13
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
14
|
+
const bot = instance.createBot("bot", {
|
|
15
|
+
username: "mybot",
|
|
16
|
+
name: "My Bot"
|
|
17
|
+
});
|
|
18
|
+
assert.deepStrictEqual(bot.identifier, "bot");
|
|
19
|
+
const response = await instance.fetch(new Request("https://example.com/ap/actor/bot", { headers: { Accept: "application/activity+json" } }));
|
|
20
|
+
assert.deepStrictEqual(response.status, 200);
|
|
21
|
+
const actor = await response.json();
|
|
22
|
+
assert.deepStrictEqual(actor.preferredUsername, "mybot");
|
|
23
|
+
assert.deepStrictEqual(actor.name, "My Bot");
|
|
24
|
+
});
|
|
25
|
+
test("serves WebFinger for a static bot", async () => {
|
|
26
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
27
|
+
instance.createBot("bot", { username: "mybot" });
|
|
28
|
+
const response = await instance.fetch(new Request("https://example.com/.well-known/webfinger?resource=acct:mybot@example.com"));
|
|
29
|
+
assert.deepStrictEqual(response.status, 200);
|
|
30
|
+
const jrd = await response.json();
|
|
31
|
+
assert.deepStrictEqual(jrd.subject, "acct:mybot@example.com");
|
|
32
|
+
});
|
|
33
|
+
test("returns 404 for unregistered identifiers", async () => {
|
|
34
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
35
|
+
instance.createBot("bot", { username: "mybot" });
|
|
36
|
+
const response = await instance.fetch(new Request("https://example.com/ap/actor/nonexistent", { headers: { Accept: "application/activity+json" } }));
|
|
37
|
+
assert.deepStrictEqual(response.status, 404);
|
|
38
|
+
});
|
|
39
|
+
test("rejects duplicate identifiers and usernames", () => {
|
|
40
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
41
|
+
instance.createBot("bot", { username: "mybot" });
|
|
42
|
+
assert.throws(() => instance.createBot("bot", { username: "other" }), TypeError);
|
|
43
|
+
assert.throws(() => instance.createBot("other", { username: "mybot" }), TypeError);
|
|
44
|
+
});
|
|
45
|
+
test("rejects usernames that differ only in case", () => {
|
|
46
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
47
|
+
instance.createBot("bot", { username: "mybot" });
|
|
48
|
+
assert.throws(() => instance.createBot("other", { username: "MyBot" }), TypeError);
|
|
49
|
+
});
|
|
50
|
+
test("serves WebFinger regardless of the username casing", async () => {
|
|
51
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
52
|
+
instance.createBot("bot", { username: "mybot" });
|
|
53
|
+
const response = await instance.fetch(new Request("https://example.com/.well-known/webfinger?resource=acct:MyBot@example.com"));
|
|
54
|
+
assert.deepStrictEqual(response.status, 200);
|
|
55
|
+
});
|
|
56
|
+
test("registers event handlers through the returned bot", () => {
|
|
57
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
58
|
+
const bot = instance.createBot("bot", { username: "mybot" });
|
|
59
|
+
const handler = () => {};
|
|
60
|
+
bot.onMention = handler;
|
|
61
|
+
assert.deepStrictEqual(bot.onMention, handler);
|
|
62
|
+
});
|
|
63
|
+
test("creates sessions for a static bot", () => {
|
|
64
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
65
|
+
const bot = instance.createBot("bot", { username: "mybot" });
|
|
66
|
+
const session = bot.getSession("https://example.com");
|
|
67
|
+
assert.deepStrictEqual(session.actorId.href, "https://example.com/ap/actor/bot");
|
|
68
|
+
assert.deepStrictEqual(session.actorHandle, "@mybot@example.com");
|
|
69
|
+
assert.deepStrictEqual(session.bot.identifier, "bot");
|
|
70
|
+
assert.deepStrictEqual(session.bot.username, "mybot");
|
|
71
|
+
});
|
|
72
|
+
test("does not redirect legacy URIs without legacyObjectUris", async () => {
|
|
73
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
74
|
+
instance.createBot("bot", { username: "mybot" });
|
|
75
|
+
const response = await instance.fetch(new Request("https://example.com/ap/note/123"));
|
|
76
|
+
assert.notDeepStrictEqual(response.status, 301);
|
|
77
|
+
});
|
|
78
|
+
test("redirects legacy URIs with legacyObjectUris", async () => {
|
|
79
|
+
const instance = createInstance({
|
|
80
|
+
kv: new MemoryKvStore(),
|
|
81
|
+
legacyObjectUris: { identifier: "bot" }
|
|
82
|
+
});
|
|
83
|
+
instance.createBot("bot", { username: "mybot" });
|
|
84
|
+
const response = await instance.fetch(new Request("https://example.com/ap/note/123"));
|
|
85
|
+
assert.deepStrictEqual(response.status, 301);
|
|
86
|
+
assert.deepStrictEqual(response.headers.get("Location"), "https://example.com/ap/actor/bot/note/123");
|
|
87
|
+
});
|
|
88
|
+
test("defines custom emojis at the instance level", () => {
|
|
89
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
90
|
+
const emojis = instance.addCustomEmojis({ wave: {
|
|
91
|
+
type: "image/png",
|
|
92
|
+
url: "https://example.com/emojis/wave.png"
|
|
93
|
+
} });
|
|
94
|
+
assert.ok("wave" in emojis);
|
|
95
|
+
assert.throws(() => instance.addCustomEmojis({ wave: {
|
|
96
|
+
type: "image/png",
|
|
97
|
+
url: "https://example.com/emojis/wave2.png"
|
|
98
|
+
} }), TypeError);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//# sourceMappingURL=instance-impl.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-impl.test.js","names":[],"sources":["../src/instance-impl.test.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2025–2026 Hong Minhee <https://hongminhee.org/>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\nimport { MemoryKvStore } from \"@fedify/fedify/federation\";\nimport assert from \"node:assert\";\nimport { describe, test } from \"node:test\";\nimport { createInstance } from \"./instance.ts\";\n\ndescribe(\"createInstance()\", () => {\n test(\"serves a static bot's actor\", async () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n const bot = instance.createBot(\"bot\", {\n username: \"mybot\",\n name: \"My Bot\",\n });\n assert.deepStrictEqual(bot.identifier, \"bot\");\n\n const response = await instance.fetch(\n new Request(\"https://example.com/ap/actor/bot\", {\n headers: { Accept: \"application/activity+json\" },\n }),\n );\n assert.deepStrictEqual(response.status, 200);\n const actor = await response.json();\n assert.deepStrictEqual(actor.preferredUsername, \"mybot\");\n assert.deepStrictEqual(actor.name, \"My Bot\");\n });\n\n test(\"serves WebFinger for a static bot\", async () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n instance.createBot(\"bot\", { username: \"mybot\" });\n const response = await instance.fetch(\n new Request(\n \"https://example.com/.well-known/webfinger?resource=acct:mybot@example.com\",\n ),\n );\n assert.deepStrictEqual(response.status, 200);\n const jrd = await response.json();\n assert.deepStrictEqual(jrd.subject, \"acct:mybot@example.com\");\n });\n\n test(\"returns 404 for unregistered identifiers\", async () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n instance.createBot(\"bot\", { username: \"mybot\" });\n const response = await instance.fetch(\n new Request(\"https://example.com/ap/actor/nonexistent\", {\n headers: { Accept: \"application/activity+json\" },\n }),\n );\n assert.deepStrictEqual(response.status, 404);\n });\n\n test(\"rejects duplicate identifiers and usernames\", () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n instance.createBot(\"bot\", { username: \"mybot\" });\n assert.throws(\n () => instance.createBot(\"bot\", { username: \"other\" }),\n TypeError,\n );\n assert.throws(\n () => instance.createBot(\"other\", { username: \"mybot\" }),\n TypeError,\n );\n });\n\n test(\"rejects usernames that differ only in case\", () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n instance.createBot(\"bot\", { username: \"mybot\" });\n // Fediverse usernames are matched case-insensitively (WebFinger acct:\n // lookups and mentions vary in casing), so two bots whose usernames\n // differ only in case would be indistinguishable:\n assert.throws(\n () => instance.createBot(\"other\", { username: \"MyBot\" }),\n TypeError,\n );\n });\n\n test(\"serves WebFinger regardless of the username casing\", async () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n instance.createBot(\"bot\", { username: \"mybot\" });\n const response = await instance.fetch(\n new Request(\n \"https://example.com/.well-known/webfinger?resource=acct:MyBot@example.com\",\n ),\n );\n assert.deepStrictEqual(response.status, 200);\n });\n\n test(\"registers event handlers through the returned bot\", () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n const bot = instance.createBot(\"bot\", { username: \"mybot\" });\n const handler = () => {};\n bot.onMention = handler;\n assert.deepStrictEqual(bot.onMention, handler);\n });\n\n test(\"creates sessions for a static bot\", () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n const bot = instance.createBot(\"bot\", { username: \"mybot\" });\n const session = bot.getSession(\"https://example.com\");\n assert.deepStrictEqual(\n session.actorId.href,\n \"https://example.com/ap/actor/bot\",\n );\n assert.deepStrictEqual(session.actorHandle, \"@mybot@example.com\");\n assert.deepStrictEqual(session.bot.identifier, \"bot\");\n assert.deepStrictEqual(session.bot.username, \"mybot\");\n });\n\n test(\"does not redirect legacy URIs without legacyObjectUris\", async () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n instance.createBot(\"bot\", { username: \"mybot\" });\n const response = await instance.fetch(\n new Request(\"https://example.com/ap/note/123\"),\n );\n assert.notDeepStrictEqual(response.status, 301);\n });\n\n test(\"redirects legacy URIs with legacyObjectUris\", async () => {\n const instance = createInstance<void>({\n kv: new MemoryKvStore(),\n legacyObjectUris: { identifier: \"bot\" },\n });\n instance.createBot(\"bot\", { username: \"mybot\" });\n const response = await instance.fetch(\n new Request(\"https://example.com/ap/note/123\"),\n );\n assert.deepStrictEqual(response.status, 301);\n assert.deepStrictEqual(\n response.headers.get(\"Location\"),\n \"https://example.com/ap/actor/bot/note/123\",\n );\n });\n\n test(\"defines custom emojis at the instance level\", () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n const emojis = instance.addCustomEmojis({\n wave: {\n type: \"image/png\",\n url: \"https://example.com/emojis/wave.png\",\n },\n });\n assert.ok(\"wave\" in emojis);\n assert.throws(\n () =>\n instance.addCustomEmojis({\n wave: {\n type: \"image/png\",\n url: \"https://example.com/emojis/wave2.png\",\n },\n }),\n TypeError,\n );\n });\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
|
|
2
|
+
import { Temporal, toTemporalInstant } from "@js-temporal/polyfill";
|
|
3
|
+
Date.prototype.toTemporalInstant = toTemporalInstant;
|
|
4
|
+
|
|
5
|
+
import { MemoryRepository } from "./repository.js";
|
|
6
|
+
import { DEFAULT_INSTANCE_ACTOR_IDENTIFIER } from "./instance-impl.js";
|
|
7
|
+
import { createInstance } from "./instance.js";
|
|
8
|
+
import { MemoryKvStore } from "@fedify/fedify/federation";
|
|
9
|
+
import { Create, Note, PUBLIC_COLLECTION } from "@fedify/vocab";
|
|
10
|
+
import assert from "node:assert";
|
|
11
|
+
import { describe, test } from "node:test";
|
|
12
|
+
|
|
13
|
+
//#region src/instance-multi.test.ts
|
|
14
|
+
function fetchJson(instance, url) {
|
|
15
|
+
return instance.fetch(new Request(url, { headers: { Accept: "application/activity+json" } })).then(async (response) => {
|
|
16
|
+
assert.deepStrictEqual(response.status, 200, `GET ${url}`);
|
|
17
|
+
return await response.json();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
describe("multiple static bots", () => {
|
|
21
|
+
test("serves distinct actors and collections", async () => {
|
|
22
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
23
|
+
instance.createBot("alpha", {
|
|
24
|
+
username: "alphabot",
|
|
25
|
+
name: "Alpha"
|
|
26
|
+
});
|
|
27
|
+
instance.createBot("beta", {
|
|
28
|
+
username: "betabot",
|
|
29
|
+
name: "Beta"
|
|
30
|
+
});
|
|
31
|
+
const alpha = await fetchJson(instance, "https://example.com/ap/actor/alpha");
|
|
32
|
+
const beta = await fetchJson(instance, "https://example.com/ap/actor/beta");
|
|
33
|
+
assert.deepStrictEqual(alpha.preferredUsername, "alphabot");
|
|
34
|
+
assert.deepStrictEqual(beta.preferredUsername, "betabot");
|
|
35
|
+
assert.notDeepStrictEqual(alpha.id, beta.id);
|
|
36
|
+
assert.notDeepStrictEqual(alpha.followers, beta.followers);
|
|
37
|
+
assert.notDeepStrictEqual(alpha.outbox, beta.outbox);
|
|
38
|
+
});
|
|
39
|
+
test("resolves each username through WebFinger", async () => {
|
|
40
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
41
|
+
instance.createBot("alpha", { username: "alphabot" });
|
|
42
|
+
instance.createBot("beta", { username: "betabot" });
|
|
43
|
+
for (const [username, identifier] of [["alphabot", "alpha"], ["betabot", "beta"]]) {
|
|
44
|
+
const response = await instance.fetch(new Request(`https://example.com/.well-known/webfinger?resource=acct:${username}@example.com`));
|
|
45
|
+
assert.deepStrictEqual(response.status, 200);
|
|
46
|
+
const jrd = await response.json();
|
|
47
|
+
const self = jrd.links.find((link) => link.rel === "self");
|
|
48
|
+
assert.deepStrictEqual(self.href, `https://example.com/ap/actor/${identifier}`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
test("does not serve one bot's objects under another bot's path", async () => {
|
|
52
|
+
const repository = new MemoryRepository();
|
|
53
|
+
const instance = createInstance({
|
|
54
|
+
kv: new MemoryKvStore(),
|
|
55
|
+
repository
|
|
56
|
+
});
|
|
57
|
+
instance.createBot("alpha", { username: "alphabot" });
|
|
58
|
+
instance.createBot("beta", { username: "betabot" });
|
|
59
|
+
const messageId = "01941f29-7c00-7fe8-ab0a-7b593990a3c0";
|
|
60
|
+
await repository.addMessage("alpha", messageId, new Create({
|
|
61
|
+
id: new URL(`https://example.com/ap/actor/alpha/create/${messageId}`),
|
|
62
|
+
actor: new URL("https://example.com/ap/actor/alpha"),
|
|
63
|
+
to: PUBLIC_COLLECTION,
|
|
64
|
+
object: new Note({
|
|
65
|
+
id: new URL(`https://example.com/ap/actor/alpha/note/${messageId}`),
|
|
66
|
+
attribution: new URL("https://example.com/ap/actor/alpha"),
|
|
67
|
+
to: PUBLIC_COLLECTION,
|
|
68
|
+
content: "Hello from alpha!"
|
|
69
|
+
})
|
|
70
|
+
}));
|
|
71
|
+
const own = await instance.fetch(new Request(`https://example.com/ap/actor/alpha/note/${messageId}`, { headers: { Accept: "application/activity+json" } }));
|
|
72
|
+
assert.deepStrictEqual(own.status, 200);
|
|
73
|
+
const cross = await instance.fetch(new Request(`https://example.com/ap/actor/beta/note/${messageId}`, { headers: { Accept: "application/activity+json" } }));
|
|
74
|
+
assert.deepStrictEqual(cross.status, 404);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("instance actor", () => {
|
|
78
|
+
test("is served under the reserved identifier", async () => {
|
|
79
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
80
|
+
instance.createBot("alpha", { username: "alphabot" });
|
|
81
|
+
const actor = await fetchJson(instance, `https://example.com/ap/actor/${DEFAULT_INSTANCE_ACTOR_IDENTIFIER}`);
|
|
82
|
+
assert.deepStrictEqual(actor.type, "Application");
|
|
83
|
+
assert.ok(actor.publicKey != null);
|
|
84
|
+
});
|
|
85
|
+
test("signs the shared inbox on multi-bot instances", () => {
|
|
86
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
87
|
+
instance.createBot("alpha", { username: "alphabot" });
|
|
88
|
+
const impl = instance.impl;
|
|
89
|
+
const ctx = impl.federation.createContext(new URL("https://example.com/"));
|
|
90
|
+
assert.deepStrictEqual(impl.dispatchSharedKey(ctx), { identifier: DEFAULT_INSTANCE_ACTOR_IDENTIFIER });
|
|
91
|
+
});
|
|
92
|
+
test("cannot be taken by a bot", async () => {
|
|
93
|
+
const instance = createInstance({ kv: new MemoryKvStore() });
|
|
94
|
+
assert.throws(() => instance.createBot(DEFAULT_INSTANCE_ACTOR_IDENTIFIER, { username: "sneaky" }), TypeError);
|
|
95
|
+
instance.createBot((_ctx, identifier) => ({ username: identifier }));
|
|
96
|
+
const response = await instance.fetch(new Request(`https://example.com/ap/actor/${DEFAULT_INSTANCE_ACTOR_IDENTIFIER}`, { headers: { Accept: "application/activity+json" } }));
|
|
97
|
+
assert.deepStrictEqual(response.status, 200);
|
|
98
|
+
const actor = await response.json();
|
|
99
|
+
assert.deepStrictEqual(actor.type, "Application");
|
|
100
|
+
});
|
|
101
|
+
test("can be renamed through instanceActorIdentifier", async () => {
|
|
102
|
+
const instance = createInstance({
|
|
103
|
+
kv: new MemoryKvStore(),
|
|
104
|
+
instanceActorIdentifier: "fetcher"
|
|
105
|
+
});
|
|
106
|
+
instance.createBot("alpha", { username: "alphabot" });
|
|
107
|
+
const actor = await fetchJson(instance, "https://example.com/ap/actor/fetcher");
|
|
108
|
+
assert.deepStrictEqual(actor.type, "Application");
|
|
109
|
+
assert.deepStrictEqual(actor.preferredUsername, "fetcher");
|
|
110
|
+
assert.throws(() => instance.createBot("fetcher", { username: "other" }), TypeError);
|
|
111
|
+
const bot = instance.createBot(DEFAULT_INSTANCE_ACTOR_IDENTIFIER, { username: "underscores" });
|
|
112
|
+
assert.deepStrictEqual(bot.identifier, DEFAULT_INSTANCE_ACTOR_IDENTIFIER);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe("instance actor key pairs", () => {
|
|
116
|
+
test("are generated once under concurrency", async () => {
|
|
117
|
+
const repository = new MemoryRepository();
|
|
118
|
+
const instance = createInstance({
|
|
119
|
+
kv: new MemoryKvStore(),
|
|
120
|
+
repository
|
|
121
|
+
});
|
|
122
|
+
instance.createBot("alpha", { username: "alphabot" });
|
|
123
|
+
const impl = instance.impl;
|
|
124
|
+
const responses = await Promise.all(Array.from({ length: 4 }, () => instance.fetch(new Request(`https://example.com/ap/actor/${DEFAULT_INSTANCE_ACTOR_IDENTIFIER}`, { headers: { Accept: "application/activity+json" } }))));
|
|
125
|
+
const actors = await Promise.all(responses.map((r) => r.json()));
|
|
126
|
+
const keyIds = new Set(actors.map((actor) => JSON.stringify(actor.publicKey)));
|
|
127
|
+
assert.deepStrictEqual(keyIds.size, 1);
|
|
128
|
+
const stored = await repository.getKeyPairs(impl.instanceActorIdentifier);
|
|
129
|
+
assert.ok(stored != null);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe("NodeInfo on multi-bot instances", () => {
|
|
133
|
+
test("counts the hosted bots", async () => {
|
|
134
|
+
const instance = createInstance({
|
|
135
|
+
kv: new MemoryKvStore(),
|
|
136
|
+
software: {
|
|
137
|
+
name: "test-bot",
|
|
138
|
+
version: "1.0.0"
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
instance.createBot("alpha", { username: "alphabot" });
|
|
142
|
+
instance.createBot("beta", { username: "betabot" });
|
|
143
|
+
const response = await instance.fetch(new Request("https://example.com/nodeinfo/2.1"));
|
|
144
|
+
assert.deepStrictEqual(response.status, 200);
|
|
145
|
+
const nodeInfo = await response.json();
|
|
146
|
+
assert.deepStrictEqual(nodeInfo.usage.users.total, 2);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//# sourceMappingURL=instance-multi.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-multi.test.js","names":["instance: InstanceWithVoidContextData","url: string","link: { rel: string }","messageId: Uuid"],"sources":["../src/instance-multi.test.ts"],"sourcesContent":["// BotKit by Fedify: A framework for creating ActivityPub bots\n// Copyright (C) 2025–2026 Hong Minhee <https://hongminhee.org/>\n//\n// This program is free software: you can redistribute it and/or modify\n// it under the terms of the GNU Affero General Public License as\n// published by the Free Software Foundation, either version 3 of the\n// License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU Affero General Public License for more details.\n//\n// You should have received a copy of the GNU Affero General Public License\n// along with this program. If not, see <https://www.gnu.org/licenses/>.\nimport { MemoryKvStore } from \"@fedify/fedify/federation\";\nimport { Create, Note, PUBLIC_COLLECTION } from \"@fedify/vocab\";\nimport assert from \"node:assert\";\nimport { describe, test } from \"node:test\";\nimport {\n createInstance,\n DEFAULT_INSTANCE_ACTOR_IDENTIFIER,\n type InstanceWithVoidContextData,\n} from \"./instance.ts\";\nimport type { InstanceImpl } from \"./instance-impl.ts\";\nimport { MemoryRepository, type Uuid } from \"./repository.ts\";\n\nfunction fetchJson(\n instance: InstanceWithVoidContextData,\n url: string,\n // deno-lint-ignore no-explicit-any\n): Promise<any> {\n return instance.fetch(\n new Request(url, { headers: { Accept: \"application/activity+json\" } }),\n ).then(async (response) => {\n assert.deepStrictEqual(response.status, 200, `GET ${url}`);\n return await response.json();\n });\n}\n\ndescribe(\"multiple static bots\", () => {\n test(\"serves distinct actors and collections\", async () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n instance.createBot(\"alpha\", { username: \"alphabot\", name: \"Alpha\" });\n instance.createBot(\"beta\", { username: \"betabot\", name: \"Beta\" });\n\n const alpha = await fetchJson(\n instance,\n \"https://example.com/ap/actor/alpha\",\n );\n const beta = await fetchJson(instance, \"https://example.com/ap/actor/beta\");\n assert.deepStrictEqual(alpha.preferredUsername, \"alphabot\");\n assert.deepStrictEqual(beta.preferredUsername, \"betabot\");\n assert.notDeepStrictEqual(alpha.id, beta.id);\n assert.notDeepStrictEqual(alpha.followers, beta.followers);\n assert.notDeepStrictEqual(alpha.outbox, beta.outbox);\n });\n\n test(\"resolves each username through WebFinger\", async () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n instance.createBot(\"alpha\", { username: \"alphabot\" });\n instance.createBot(\"beta\", { username: \"betabot\" });\n\n for (\n const [username, identifier] of [\n [\"alphabot\", \"alpha\"],\n [\"betabot\", \"beta\"],\n ]\n ) {\n const response = await instance.fetch(\n new Request(\n `https://example.com/.well-known/webfinger?resource=acct:${username}@example.com`,\n ),\n );\n assert.deepStrictEqual(response.status, 200);\n const jrd = await response.json();\n const self = jrd.links.find((link: { rel: string }) =>\n link.rel === \"self\"\n );\n assert.deepStrictEqual(\n self.href,\n `https://example.com/ap/actor/${identifier}`,\n );\n }\n });\n\n test(\"does not serve one bot's objects under another bot's path\", async () => {\n const repository = new MemoryRepository();\n const instance = createInstance<void>({\n kv: new MemoryKvStore(),\n repository,\n });\n instance.createBot(\"alpha\", { username: \"alphabot\" });\n instance.createBot(\"beta\", { username: \"betabot\" });\n\n const messageId: Uuid = \"01941f29-7c00-7fe8-ab0a-7b593990a3c0\";\n await repository.addMessage(\n \"alpha\",\n messageId,\n new Create({\n id: new URL(\n `https://example.com/ap/actor/alpha/create/${messageId}`,\n ),\n actor: new URL(\"https://example.com/ap/actor/alpha\"),\n to: PUBLIC_COLLECTION,\n object: new Note({\n id: new URL(`https://example.com/ap/actor/alpha/note/${messageId}`),\n attribution: new URL(\"https://example.com/ap/actor/alpha\"),\n to: PUBLIC_COLLECTION,\n content: \"Hello from alpha!\",\n }),\n }),\n );\n\n const own = await instance.fetch(\n new Request(\n `https://example.com/ap/actor/alpha/note/${messageId}`,\n { headers: { Accept: \"application/activity+json\" } },\n ),\n );\n assert.deepStrictEqual(own.status, 200);\n\n // The same UUID under beta's path must not leak alpha's message:\n const cross = await instance.fetch(\n new Request(\n `https://example.com/ap/actor/beta/note/${messageId}`,\n { headers: { Accept: \"application/activity+json\" } },\n ),\n );\n assert.deepStrictEqual(cross.status, 404);\n });\n});\n\ndescribe(\"instance actor\", () => {\n test(\"is served under the reserved identifier\", async () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n instance.createBot(\"alpha\", { username: \"alphabot\" });\n const actor = await fetchJson(\n instance,\n `https://example.com/ap/actor/${DEFAULT_INSTANCE_ACTOR_IDENTIFIER}`,\n );\n assert.deepStrictEqual(actor.type, \"Application\");\n assert.ok(actor.publicKey != null);\n });\n\n test(\"signs the shared inbox on multi-bot instances\", () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n instance.createBot(\"alpha\", { username: \"alphabot\" });\n const impl = (instance as unknown as {\n impl: {\n dispatchSharedKey(ctx: unknown): { identifier: string };\n federation: {\n createContext(url: URL): unknown;\n };\n };\n }).impl;\n const ctx = impl.federation.createContext(new URL(\"https://example.com/\"));\n assert.deepStrictEqual(impl.dispatchSharedKey(ctx), {\n identifier: DEFAULT_INSTANCE_ACTOR_IDENTIFIER,\n });\n });\n\n test(\"cannot be taken by a bot\", async () => {\n const instance = createInstance<void>({ kv: new MemoryKvStore() });\n assert.throws(\n () =>\n instance.createBot(DEFAULT_INSTANCE_ACTOR_IDENTIFIER, {\n username: \"sneaky\",\n }),\n TypeError,\n );\n\n // Dynamic dispatchers cannot take the reserved identifier either:\n instance.createBot((_ctx, identifier) => ({ username: identifier }));\n const response = await instance.fetch(\n new Request(\n `https://example.com/ap/actor/${DEFAULT_INSTANCE_ACTOR_IDENTIFIER}`,\n { headers: { Accept: \"application/activity+json\" } },\n ),\n );\n assert.deepStrictEqual(response.status, 200);\n const actor = await response.json();\n assert.deepStrictEqual(actor.type, \"Application\");\n });\n\n test(\"can be renamed through instanceActorIdentifier\", async () => {\n const instance = createInstance<void>({\n kv: new MemoryKvStore(),\n instanceActorIdentifier: \"fetcher\",\n });\n instance.createBot(\"alpha\", { username: \"alphabot\" });\n\n const actor = await fetchJson(\n instance,\n \"https://example.com/ap/actor/fetcher\",\n );\n assert.deepStrictEqual(actor.type, \"Application\");\n assert.deepStrictEqual(actor.preferredUsername, \"fetcher\");\n\n // The custom identifier is the reserved one now:\n assert.throws(\n () => instance.createBot(\"fetcher\", { username: \"other\" }),\n TypeError,\n );\n // ...and the default identifier is free for bots:\n const bot = instance.createBot(DEFAULT_INSTANCE_ACTOR_IDENTIFIER, {\n username: \"underscores\",\n });\n assert.deepStrictEqual(bot.identifier, DEFAULT_INSTANCE_ACTOR_IDENTIFIER);\n });\n});\n\ndescribe(\"instance actor key pairs\", () => {\n test(\"are generated once under concurrency\", async () => {\n const repository = new MemoryRepository();\n const instance = createInstance<void>({\n kv: new MemoryKvStore(),\n repository,\n });\n instance.createBot(\"alpha\", { username: \"alphabot\" });\n const impl = (instance as unknown as {\n impl: InstanceImpl<void>;\n }).impl;\n // Simulates concurrent cold-start requests hitting the key pair\n // dispatcher at once:\n const responses = await Promise.all(\n Array.from({ length: 4 }, () =>\n instance.fetch(\n new Request(\n `https://example.com/ap/actor/${DEFAULT_INSTANCE_ACTOR_IDENTIFIER}`,\n { headers: { Accept: \"application/activity+json\" } },\n ),\n )),\n );\n const actors = await Promise.all(responses.map((r) => r.json()));\n const keyIds = new Set(\n actors.map((actor) => JSON.stringify(actor.publicKey)),\n );\n // Every response advertises the same key, and the stored key matches:\n assert.deepStrictEqual(keyIds.size, 1);\n const stored = await repository.getKeyPairs(\n impl.instanceActorIdentifier,\n );\n assert.ok(stored != null);\n });\n});\n\ndescribe(\"NodeInfo on multi-bot instances\", () => {\n test(\"counts the hosted bots\", async () => {\n const instance = createInstance<void>({\n kv: new MemoryKvStore(),\n software: { name: \"test-bot\", version: \"1.0.0\" },\n });\n instance.createBot(\"alpha\", { username: \"alphabot\" });\n instance.createBot(\"beta\", { username: \"betabot\" });\n const response = await instance.fetch(\n new Request(\"https://example.com/nodeinfo/2.1\"),\n );\n assert.deepStrictEqual(response.status, 200);\n const nodeInfo = await response.json();\n assert.deepStrictEqual(nodeInfo.usage.users.total, 2);\n });\n});\n"],"mappings":"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"}
|