notify is core again

built 2026-07-31, switched off 2026-08-13 when it was slated to become a
plugin alongside memos and photos. those two were extracted; this one never
was, so it spent six days mounted nowhere with its schema commented out and
no pm2 entry — complete code that nothing could reach.

core rather than a plugin because the producers are the platform's own: the
job queue, an agent turn ending, mail arriving. a notifier the owner can
uninstall is one those producers cannot rely on.

- hono.ts mounts /api/notify again; verified 401 unauthenticated, so the
  permission gate covers it and assertPermissionTotality accepts the mount
- push_devices exported from the core schema and pushed: 10 columns,
  uq_push_devices_token_bundle, idx_push_devices_user
- officer-notify added to CORE_PROCESSES, so gen:ecosystem writes it
- credentials read OFFICERDEV_X ?? X, the convention it predates

safe as core because it boots with nothing configured: every channel reports
isConfigured() false and dispatch resolves empty. verified by importing it
with the env stripped — "channels: none configured", no throw. an install
with no apns key runs it as a no-op rather than crash-looping.

APNS_ENV is dropped from the doc's credential table. it was never read: the
host comes from each device's `environment` column, so one server pushes to a
testflight build and a release build at once.

doc status corrected. the app side is not started — there is no officer-suite
repo, no PUSH_NOTIFICATIONS_APP.md, and apps/chat actively strips the ios push
entitlement. recorded there rather than here.
This commit is contained in:
2026-08-19 18:05:33 +00:00
parent ab636eaa42
commit fd7c9becb6
8 changed files with 71 additions and 38 deletions
+1 -1
View File
@@ -43,10 +43,10 @@ export * from './agent-panels/schema'; // agent_panels
export * from './app-store/schema'; // sidecar_installs
export * from './plugins/schema'; // plugin_installs — core: the plugin system is the platform's own
export * from './service-connections/schema'; // service_connections
export * from './notify/schema'; // push_devices — core since 2026-08-19, see docs/push-notifications.md
// ── Plugins — uncomment when the plugin is installed ─────────────────────────────────────────────
// export * from './notify/schema'; // push_devices officer-notify
export * from './dav/schema'; // dav_app_passwords officer-caldav
// ── Plugin tables are NOT here ───────────────────────────────────────────────────────────────────
+2 -2
View File
@@ -31,7 +31,7 @@ import { appStoreRouter } from './api/app-store/router';
import { davSyncRouter } from './api/dav/sync-router';
import { davRouter } from './api/dav/router';
import { claimIosProfile } from './api/dav/ios-profile';
// import { notifyRouter } from './api/notify/router';
import { notifyRouter } from './api/notify/router';
import { systemMonitorRouter } from './api/system-monitor/system-monitor';
import { activityRouter } from './api/activity/router';
import { dockRouter } from './api/dock/dock';
@@ -142,7 +142,7 @@ const PROTECTED_MOUNTS: [prefix: string, router: ReturnType<typeof createRouter>
['/plugins', pluginsRouter],
['/caldav', caldavRouter], // the JSON door for Officer's own calendar/contacts UI
['/dav', davRouter], // app-password management (the sync door is /dav, top-level)
// ['/notify', notifyRouter], // plugin — switched off 2026-08-13
['/notify', notifyRouter], // core since 2026-08-19 — the one outbound-notification surface
// ['/photos', photosRouter], // plugin — switched off 2026-08-13
['/system-monitor', systemMonitorRouter],
['/activity', activityRouter],
+1 -2
View File
@@ -167,8 +167,7 @@ const CORE_REGISTRY: Permission[] = [
label: 'Notifications',
description: 'Push notifications to your devices',
kind: 'app',
// api: ['/notify'], // plugin — switched off 2026-08-13
api: [],
api: ['/notify'],
// Device registration is the caller's own — a phone subscribing to its own push channel.
personal: ['/devices', '/subscriptions'],
},
+4 -1
View File
@@ -22,7 +22,10 @@ const HOSTS = {
/** Apple's reasons for "this token is dead" — the row goes immediately rather than after three strikes. */
const DEAD_TOKEN_REASONS = new Set(['BadDeviceToken', 'Unregistered', 'DeviceTokenNotForTopic']);
const { APNS_KEY_P8, APNS_KEY_ID, APNS_TEAM_ID } = process.env;
// `OFFICERDEV_X ?? X` — the house convention since the env prefixing, so an install that predates it
// keeps working. See src/servers/env-prefix.ts.
const env = (name: string) => process.env[`OFFICERDEV_${name}`] ?? process.env[name];
const [APNS_KEY_P8, APNS_KEY_ID, APNS_TEAM_ID] = [env('APNS_KEY_P8'), env('APNS_KEY_ID'), env('APNS_TEAM_ID')];
const auth =
APNS_KEY_P8 && APNS_KEY_ID && APNS_TEAM_ID
+1 -1
View File
@@ -15,7 +15,7 @@ import { renderTitle } from './text';
// and its history is searchable and permanent in a way a push banner is not — arguably it deserves the
// rule more, not less.
const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;
const WEBHOOK_URL = process.env.OFFICERDEV_DISCORD_WEBHOOK_URL ?? process.env.DISCORD_WEBHOOK_URL;
export const discordChannel: Channel = {
name: 'discord',
+1 -1
View File
@@ -18,7 +18,7 @@ const ENDPOINT = (projectId: string) => `https://fcm.googleapis.com/v1/projects/
/** Google's ways of saying "this token is dead" — the row goes immediately. */
const DEAD_TOKEN_CODES = new Set(['UNREGISTERED', 'INVALID_ARGUMENT', 'NOT_FOUND']);
const raw = process.env.FCM_SERVICE_ACCOUNT;
const raw = process.env.OFFICERDEV_FCM_SERVICE_ACCOUNT ?? process.env.FCM_SERVICE_ACCOUNT;
const auth = raw ? new FcmAccessToken(parseServiceAccount(raw)) : null;
type FcmMessage = {