dl.f3liz.casa/drop を読んでいます
newtab
newtab actor(Activity Stream のデータを新しいタブに渡す)。built-in と同じ source。registry の一周目
uuid ec4dfa7c-9e5a-4c1d-8d0d-771e3ee81030
install
Press inside noraneko. It catches the link, opens its settings with this uuid, downloads the xpi, checks the sha256 and the registry's signature, and shows you everything below again before anything runs.
about:nora:settings in noraneko instead.
who
gh/nyanruswhat is served
newtab · about-newtab@noraneko.app · version 1.0.2 · 9146 bytes sha256 d134afd34b3f0f18f37fc020116b35bc48c77288512e01f75a21522c65d0ca05 built from drops/newtab/src @ 59b96c854c (2026-09-14T07:24:00Z) served at https://dl.f3liz.casa/drop/ec4dfa7c-9e5a-4c1d-8d0d-771e3ee81030/in the registry
drops/newtab/ @ 59b96c854c — この版が出てきた木 actors: newtabthe source, as it is
These come from source.json next to the manifest — the same text the xpi carries under source/, and dl checks its sha256 against the signed manifest before handing it over. Not a copy of the repository: what you read and what runs came out of the same build. 4 files; press one to open it.
_shared/contentRuntime.ts · 50 lines
// SPDX-License-Identifier: MPL-2.0
// Shared page-side runtime. Bundled into every actor's content.js.
//
// content.js is loaded by the generated child.sys.mjs (a JSWindowActorChild) with
// Services.scriptloader.loadSubScript(url, scope). `scope` puts `window`,
// `document`, `exportFunction` and `__nora` on the scope chain, so the actor's
// content hook can use `window` / `document` as free variables just like a
// content script would, while actually running with the child actor's
// privileges in the page's process. `__nora` is the bridge to the parent
// (sendQuery) and to the page (exportFunction).
import type { ActorMeta, ContentCtx, ContentHook, Ops } from "./defineActor.ts";
import { makeIo } from "./io.ts";
// Provided on the scope chain by child.sys.mjs.
declare const __nora: {
call(method: string, args: unknown[]): Promise<unknown>;
expose(funcs: Record<string, (...args: any[]) => unknown>): void;
onDestroy(fn: () => void): void;
base: string;
tsubaki: Ops | undefined;
};
export function runContent(_meta: ActorMeta, hook: ContentHook): void {
const parent = new Proxy(
{},
{
get(_target, method: string) {
return (...args: unknown[]) => __nora.call(method, args);
},
},
) as any;
const ctx: ContentCtx = {
dev: import.meta.env.MODE === "dev",
expose(funcs) {
__nora.expose(funcs);
},
onDestroy(fn) {
__nora.onDestroy(fn);
},
io: makeIo((fn) => __nora.onDestroy(fn)),
base: __nora.base,
ops: __nora.tsubaki,
};
hook(parent, ctx);
}
_shared/defineActor.ts · 118 lines
// SPDX-License-Identifier: MPL-2.0
// Authoring surface for an "actor": a small xpi whose page side is a
// JSWindowActor (the same shape as Firefox's own about:newtab add-on). Each
// actor lives in one file that exports three things by name:
//
// export const meta = { id, namespace, matches, ... };
// export const parent = defineParent({ method(...) { ... } }); // main process (JSWindowActorParent)
// export const content = defineContent((parent, ctx) => { ... }); // page side (run by JSWindowActorChild)
//
// The page side is NOT a WebExtension content script: content scripts are not
// injected into about:* / chrome:* documents in stock Firefox (see README,
// "addon 式が駄目だった理由"). The child actor loads content.js into the page's
// process with `window` / `document` on the scope chain, so the hook is written
// the same way, but it runs with the child actor's privileges.
//
// Named exports (rather than one nested object) are deliberate: the build
// bundles `parent` and `content` into separate outputs, and named exports let
// the bundler tree-shake each side so the content bundle never drags in the
// parent's privileged code and vice versa.
//
// IMPORTANT: the module top level must be pure. Reference Firefox globals
// (Services, ChromeUtils, window, exportFunction, browser) only *inside* method
// or hook bodies — the build imports this file under Deno to read `meta`, and
// top-level use of those globals would throw there.
export interface ActorMeta {
/** Extension id, e.g. "settings-bridge@noraneko.app". */
id: string;
/** Extension version. */
version: string;
/** Experiment-API namespace, e.g. "noraSettings". Also the message channel. */
namespace: string;
/** Match patterns for the pages the content hook runs in (JSWindowActor `matches`). */
matches: string[];
/**
* When the content hook runs. Maps to a child actor event:
* document_start → DOMDocElementInserted, document_end → DOMContentLoaded,
* document_idle → load. Defaults to "document_end".
*/
runAt?: "document_start" | "document_end" | "document_idle";
/**
* JSWindowActor name. Defaults to "Nora" + PascalCase(<dir>), e.g. NoraNewtab.
* A drop with the same actor name replaces the built-in one (unregister → register).
*/
actor?: string;
/** Legacy JSActor (BrowserGlue) this one replaces; it is unregistered when this actor is registered. */
replaces?: string;
}
import type { Io } from "./io.ts";
type ParentMethods = Record<string, (...args: any[]) => unknown>;
export interface ContentCtx {
/** True in dev builds (import.meta.env.MODE === "dev"). */
dev: boolean;
/** Expose functions on the page window (via exportFunction). */
expose(funcs: Record<string, (...args: any[]) => unknown>): void;
/**
* Run when this actor goes away in this window (the drop is removed or
* replaced). Put back what the hook changed: DOM, observers, listeners.
*/
onDestroy(fn: () => void): void;
/**
* Place things through these (DOM, styles, listeners, pref observers) and
* they are taken out again by themselves: each verb puts its way back on
* the same ledger as onDestroy.
*/
io: Io;
/** This actor's own files: `${ctx.base}ops/x.tsubaki` and the like. */
base: string;
/**
* The actor's logic in Tsubaki (when it ships wasm/): run a .tsubaki file of
* its own with load("ops/x.tsubaki"), then call(name, ...args). Values cross
* as ordinary JS values; a Tsubaki error is a JS Error. undefined without wasm/.
*
* Every verb answers with a Promise, because for an actor of the browser
* WINDOW the logic lives a process away (wasm can't compile in the parent
* process, so it runs in a hidden page of this window instead). A drop never
* has to know which side it got.
*/
ops: Ops | undefined;
}
export interface Ops {
ready: Promise<void>;
call(name: string, ...args: unknown[]): Promise<unknown>;
}
/** Proxy to the parent methods; each call is forwarded to the main process. */
type ParentProxy<T extends ParentMethods> = {
[K in keyof T]: (...args: Parameters<T[K]>) => Promise<Awaited<ReturnType<T[K]>>>;
};
export type ContentHook<T extends ParentMethods = ParentMethods> = (
parent: ParentProxy<T>,
ctx: ContentCtx,
) => void;
// Identity helpers that pin types for authoring:
//
// export const parent = defineParent({ method(...) { ... } });
// export const content = defineContent<typeof parent>((parent, ctx) => { ... });
//
// They are listed in the build's treeshake.manualPureFunctions, so an unused
// call (e.g. defineContent in the parent bundle) is dropped along with anything
// it references (birpc). Without that, the bundler would keep the call for its
// possible side effects and the unused side would not tree-shake.
/** Pins the parent-method types so the content hook's proxy is typed. */
export const defineParent = <T extends ParentMethods>(methods: T): T => methods;
/** Pins the content hook against the parent's method types. */
export const defineContent = <T extends ParentMethods>(
hook: ContentHook<T>,
): ContentHook<T> => hook;
_shared/io.ts · 53 lines
// SPDX-License-Identifier: MPL-2.0
// The io layer of a content hook: every verb here *places* something in the
// window and, in the same breath, puts the way back on the ledger
// (ctx.onDestroy). When the drop is removed or replaced, the ledger is run in
// reverse and the window is as it was. A hook that only ever places through
// these verbs has no cleanup to write.
export interface Io {
/** Insert a node and remember to remove it. */
place(node: Node, at: { parent: Node } | { before: Node } | { after: Node }): void;
/** Add a <style> to the document head and remember to remove it. */
style(doc: Document, css: string): HTMLStyleElement;
/** addEventListener, and remember the matching removeEventListener. */
listen<K extends string>(
target: EventTarget,
type: K,
fn: (ev: any) => void,
options?: boolean | AddEventListenerOptions,
): void;
/** Watch a pref and remember to stop watching. */
pref(name: string, fn: () => void): void;
/** Anything else that has to be put back: hand the way back to the ledger. */
defer(fn: () => void): void;
}
export function makeIo(onDestroy: (fn: () => void) => void): Io {
return {
place(node, at) {
if ("parent" in at) at.parent.appendChild(node);
else if ("before" in at) (at.before as ChildNode).before(node);
else (at.after as ChildNode).after(node);
onDestroy(() => (node as ChildNode).remove());
},
style(doc, css) {
const el = doc.createElementNS("http://www.w3.org/1999/xhtml", "style") as HTMLStyleElement;
el.textContent = css;
this.place(el, { parent: doc.head ?? doc.documentElement });
return el;
},
listen(target, type, fn, options) {
target.addEventListener(type, fn, options);
onDestroy(() => target.removeEventListener(type, fn, options));
},
pref(name, fn) {
const observer = () => fn();
Services.prefs.addObserver(name, observer);
onDestroy(() => Services.prefs.removeObserver(name, observer));
},
defer: onDestroy,
};
}
newtab/actor.ts · 56 lines
// SPDX-License-Identifier: MPL-2.0
// newtab: feed privileged Activity Stream data (top sites, highlights) to the
// noraneko new tab page. Replaces the NRAboutNewTab JSActor.
//
// Firefox ships about:newtab itself as a built-in WebExtension; this mirrors
// the "deliver privileged data to the page" half. The page assets + about:
// registration stay where they are (NoranekoStartup / chrome://noraneko-newtab).
import {
defineContent,
defineParent,
type ActorMeta,
} from "../_shared/defineActor.ts";
export const meta: ActorMeta = {
id: "about-newtab@noraneko.app",
version: "1.0.2",
namespace: "noraNewTab",
matches: ["about:home*", "about:welcome", "about:newtab*"],
runAt: "document_start",
};
export const parent = defineParent({
async getData(): Promise<{ topSites: unknown[]; highlights: unknown[] }> {
try {
const { NewTabUtils } = ChromeUtils.importESModule(
"resource://gre/modules/NewTabUtils.sys.mjs",
);
const [topSites, highlights] = await Promise.all([
NewTabUtils.activityStreamLinks.getTopSites({
withFavicons: true,
numItems: 16,
}),
NewTabUtils.activityStreamLinks.getHighlights({
withFavicons: true,
numItems: 16,
}),
]);
return { topSites: topSites ?? [], highlights: highlights ?? [] };
} catch (e) {
console.error("[noraneko] newtab: failed to load data:", e);
return { topSites: [], highlights: [] };
}
},
});
export const content = defineContent<typeof parent>((parent) => {
window.addEventListener("DOMContentLoaded", async () => {
const data = await parent.getData();
window.dispatchEvent(
new window.CustomEvent("noranekoNewtabData", { detail: data }),
);
});
});
What a drop is, how it reaches you, and what it is not: f3liz.casa/noraneko/drops/. Served by https://dl.f3liz.casa/drop, signed by https://github.com/f3liz-casa/noraneko-registry/.github/workflows/verify-and-sign.yml@refs/heads/main.
This page is built when you ask for it, out of what https://dl.f3liz.casa/drop is serving right now (2026-09-23T08:38:28.032Z). Drafted by Shiro, an AI assistant, sitting next to nyanrus.