Skip to content

Commit bdb7cd4

Browse files
authored
feat(push): reader content and reply notification fan-out (#2812)
* feat(push): add reader content and reply notification fan-out Extend push-protocol, relay bindings/preferences, and core push APIs so Yohaku readers can receive published content and comment-reply alerts without changing Space admin push. * feat(push): scope relay binding API to the owning installation Devices now read, update, and revoke their own binding with Installation credentials, and a source claim no longer overwrites device-owned preferences while it replaces or clears reader_id. * refactor(push): anonymous activation with optional reader association Remove reader-scoped push endpoints and PushAuth in favor of a single public POST /notifications/push/activate that claims relay tickets with an optional session reader_id. Relay owns device binding preferences; core stores source metadata with nullable owner_id. * test(push): assert single public activate route in OpenAPI manifest Also simplify push repository PG e2e owner lookup with a direct eq() import. * fix(push): make the rewritten binding-owner migration actually apply Migration 0033 was rewritten in place from an expand-only push_reader_preferences table into the owner_id relaxation, but kept the abandoned revision's journal timestamp. The schema migrator draws its waterline from ledger rows that match no bundled hash, so any database that ran the abandoned revision recorded the rewrite as applied and silently skipped its SQL, leaving owner_id NOT NULL and the orphaned preferences table in place. Re-tag it with a later timestamp, drop the abandoned table, and repair the 0033 snapshot id/prevId that had been copied from 0032 (drizzle-kit check reported the collision). Activation also takes the optional reader id last, matching how the other public reader-aware controllers declare it. * feat(push): enrich reply and content notifications for APNs UI Project public display metadata into push events, skip content publishes without a summary, and build localized APNs payloads with mutable-content for Communication Notifications on comment replies. * fix(push): close review blockers before reader push merge Prevent private content metadata from reaching push payloads, reject encoded path traversal, and move Space iOS binding status and revoke operations to the installation-authenticated Relay API after removing the legacy core routes.
1 parent 8fa0c6f commit bdb7cd4

48 files changed

Lines changed: 11016 additions & 622 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ run
4646
/data
4747
/assets
4848
.env
49+
.env.local
50+
apps/push-relay/.env.local
4951

5052
scripts/workflow/docker-compose.yml
5153
scripts/workflow/data

apps/core/openapi.json

Lines changed: 1 addition & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -245,16 +245,11 @@
245245
"/notifications/push/activate": {
246246
"post": {
247247
"operationId": "activatePushNotifications",
248-
"summary": "Bind this authenticated owner installation to a Push Relay",
248+
"summary": "Claim a Push Relay activation ticket for this device, optionally associating the current reader",
249249
"tags": [
250250
"notifications"
251251
],
252252
"parameters": [],
253-
"security": [
254-
{
255-
"bearerAuth": []
256-
}
257-
],
258253
"requestBody": {
259254
"required": true,
260255
"content": {
@@ -310,115 +305,6 @@
310305
}
311306
}
312307
},
313-
"/notifications/push/status": {
314-
"get": {
315-
"operationId": "getPushNotificationStatus",
316-
"summary": "Read the current owner push binding status",
317-
"tags": [
318-
"notifications"
319-
],
320-
"parameters": [],
321-
"security": [
322-
{
323-
"bearerAuth": []
324-
}
325-
],
326-
"responses": {
327-
"200": {
328-
"description": "OK",
329-
"content": {
330-
"application/json": {
331-
"schema": {
332-
"type": "object",
333-
"required": [
334-
"data"
335-
],
336-
"properties": {
337-
"data": {
338-
"$ref": "#/components/schemas/PushStatus"
339-
},
340-
"meta": {
341-
"$ref": "#/components/schemas/ResponseMeta"
342-
}
343-
}
344-
}
345-
}
346-
}
347-
},
348-
"4XX": {
349-
"description": "Error",
350-
"content": {
351-
"application/json": {
352-
"schema": {
353-
"$ref": "#/components/schemas/ErrorEnvelope"
354-
}
355-
}
356-
}
357-
},
358-
"5XX": {
359-
"description": "Error",
360-
"content": {
361-
"application/json": {
362-
"schema": {
363-
"$ref": "#/components/schemas/ErrorEnvelope"
364-
}
365-
}
366-
}
367-
}
368-
}
369-
}
370-
},
371-
"/notifications/push/{binding_id}": {
372-
"delete": {
373-
"operationId": "deactivatePushNotifications",
374-
"summary": "Revoke one mobile push binding",
375-
"tags": [
376-
"notifications"
377-
],
378-
"parameters": [
379-
{
380-
"name": "binding_id",
381-
"in": "path",
382-
"required": true,
383-
"schema": {
384-
"type": "string",
385-
"minLength": 1,
386-
"maxLength": 128
387-
}
388-
}
389-
],
390-
"security": [
391-
{
392-
"bearerAuth": []
393-
}
394-
],
395-
"responses": {
396-
"204": {
397-
"description": "No Content"
398-
},
399-
"4XX": {
400-
"description": "Error",
401-
"content": {
402-
"application/json": {
403-
"schema": {
404-
"$ref": "#/components/schemas/ErrorEnvelope"
405-
}
406-
}
407-
}
408-
},
409-
"5XX": {
410-
"description": "Error",
411-
"content": {
412-
"application/json": {
413-
"schema": {
414-
"$ref": "#/components/schemas/ErrorEnvelope"
415-
}
416-
}
417-
}
418-
}
419-
}
420-
}
421-
},
422308
"/aggregate/desk": {
423309
"get": {
424310
"operationId": "getDesk",
@@ -2236,29 +2122,6 @@
22362122
],
22372123
"additionalProperties": false
22382124
},
2239-
"PushStatus": {
2240-
"type": "object",
2241-
"properties": {
2242-
"configured": {
2243-
"type": "boolean"
2244-
},
2245-
"enabled": {
2246-
"type": "boolean"
2247-
},
2248-
"relay_url": {
2249-
"type": "string",
2250-
"format": "uri"
2251-
},
2252-
"binding_id": {
2253-
"type": "string"
2254-
}
2255-
},
2256-
"required": [
2257-
"configured",
2258-
"enabled"
2259-
],
2260-
"additionalProperties": false
2261-
},
22622125
"Desk": {
22632126
"type": "object",
22642127
"properties": {

apps/core/src/common/openapi/route-manifest.ts

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ import { NoteViews } from '~/modules/note/note.views'
2626
import {
2727
PushActivationRequestSchema,
2828
PushActivationResponseSchema,
29-
PushBindingIdParamSchema,
30-
PushStatusResponseSchema,
3129
} from '~/modules/push/push.schema'
3230
import {
3331
RecentlyRefCandidatesQuerySchema,
@@ -148,8 +146,9 @@ export const routeManifest: readonly OpenApiRoute[] = [
148146
method: 'post',
149147
path: '/notifications/push/activate',
150148
tag: 'notifications',
151-
summary: 'Bind this authenticated owner installation to a Push Relay',
152-
auth: true,
149+
summary:
150+
'Claim a Push Relay activation ticket for this device, optionally associating the current reader',
151+
auth: false,
153152
body: {
154153
name: 'PushActivationRequest',
155154
schema: PushActivationRequestSchema,
@@ -159,27 +158,6 @@ export const routeManifest: readonly OpenApiRoute[] = [
159158
schema: PushActivationResponseSchema,
160159
},
161160
},
162-
{
163-
operationId: 'getPushNotificationStatus',
164-
method: 'get',
165-
path: '/notifications/push/status',
166-
tag: 'notifications',
167-
summary: 'Read the current owner push binding status',
168-
auth: true,
169-
response: {
170-
name: 'PushStatus',
171-
schema: PushStatusResponseSchema,
172-
},
173-
},
174-
{
175-
operationId: 'deactivatePushNotifications',
176-
method: 'delete',
177-
path: '/notifications/push/:bindingId',
178-
tag: 'notifications',
179-
summary: 'Revoke one mobile push binding',
180-
auth: true,
181-
params: PushBindingIdParamSchema,
182-
},
183161

184162
{
185163
operationId: 'getDesk',
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- An earlier revision of this migration created "push_reader_preferences"
2+
-- before Relay took ownership of per-device preferences. Databases that applied
3+
-- it still carry the table, so retire it here rather than leaving it orphaned.
4+
DROP TABLE IF EXISTS "push_reader_preferences";
5+
--> statement-breakpoint
6+
ALTER TABLE "push_relay_bindings" DROP CONSTRAINT "push_relay_bindings_owner_id_readers_id_fk";
7+
--> statement-breakpoint
8+
ALTER TABLE "push_relay_bindings" ALTER COLUMN "owner_id" DROP NOT NULL;
9+
--> statement-breakpoint
10+
ALTER TABLE "push_relay_bindings" ADD CONSTRAINT "push_relay_bindings_owner_id_readers_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."readers"("id") ON DELETE set null ON UPDATE no action;

0 commit comments

Comments
 (0)