Skip to content

Commit b50cbac

Browse files
committed
Add push notification callbacks to interaction processing.
1 parent 067e637 commit b50cbac

File tree

2 files changed

+101
-39
lines changed

2 files changed

+101
-39
lines changed

lib/interactions.js

Lines changed: 96 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44
import * as bedrock from '@bedrock/core';
55
import * as schemas from '../schemas/bedrock-profile-http.js';
6-
import {poll, pollers} from '@bedrock/notify';
6+
import {poll, pollers, push} from '@bedrock/notify';
77
import {agent} from '@bedrock/https-agent';
88
import {asyncHandler} from '@bedrock/express';
99
import {ensureAuthenticated} from '@bedrock/passport';
@@ -16,6 +16,12 @@ const {config, util: {BedrockError}} = bedrock;
1616
let DEFINITIONS_BY_TYPE_MAP;
1717
let DEFINITIONS_BY_ID_MAP;
1818

19+
// use a TTL of 1 second to account for the case where a push notification
20+
// isn't received by the same instance that the client hits, but prevent
21+
// requests from triggering a hit to the workflow service backend more
22+
// frequently than 1 second
23+
const POLL_TTL = 1000;
24+
1925
bedrock.events.on('bedrock.init', () => {
2026
const cfg = config['profile-http'];
2127

@@ -65,9 +71,13 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
6571
const interactionsPath = '/interactions';
6672
const routes = {
6773
interactions: interactionsPath,
68-
interaction: `${interactionsPath}/:localInteractionId/:localExchangeId`
74+
interaction: `${interactionsPath}/:localInteractionId/:localExchangeId`,
75+
callback: `${interactionsPath}/:localInteractionId/callbacks/:pushToken`
6976
};
7077

78+
// base URL for server
79+
const {baseUri} = bedrock.config.server;
80+
7181
// create an interaction to exchange VCs
7282
app.post(
7383
routes.interactions,
@@ -88,6 +98,15 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
8898
});
8999
}
90100

101+
// create a push token
102+
const {token} = await push.createPushToken({event: 'exchangeUpdated'});
103+
104+
// compute callback URL
105+
const {localInteractionId} = definition;
106+
const callbackUrl =
107+
`${baseUri}${interactionsPath}/${localInteractionId}` +
108+
`/callbacks/${token}`;
109+
91110
// create exchange with given variables
92111
const exchange = {
93112
// FIXME: use `expires` instead of now-deprecated `ttl`
@@ -96,13 +115,15 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
96115
// template variables
97116
variables: {
98117
...variables,
118+
callback: {
119+
url: callbackUrl
120+
},
99121
accountId
100122
}
101123
};
102124
const capability = definition.zcaps.get('readWriteExchanges');
103125
const response = await zcapClient.write({json: exchange, capability});
104126
const exchangeId = response.headers.get('location');
105-
const {localInteractionId} = definition;
106127
// reuse `localExchangeId` in path
107128
const localExchangeId = exchangeId.slice(exchangeId.lastIndexOf('/'));
108129
const id = `${config.server.baseUri}/${routes.interactions}/` +
@@ -123,14 +144,7 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
123144
} = req;
124145

125146
// get interaction definition
126-
const definition = DEFINITIONS_BY_ID_MAP.get(localInteractionId);
127-
if(!definition) {
128-
throw new BedrockError(
129-
`Interaction type for "${localInteractionId}" not found.`, {
130-
name: 'NotFoundError',
131-
details: {httpStatusCode: 404, public: true}
132-
});
133-
}
147+
const definition = _getInteractionDefinition({localInteractionId});
134148

135149
// determine full exchange ID based on related capability
136150
const capability = definition.zcaps.get('readWriteExchanges');
@@ -162,36 +176,79 @@ bedrock.events.on('bedrock-express.configure.routes', app => {
162176
// poll the exchange...
163177
const result = await poll({
164178
id: exchangeId,
165-
poller: pollers.createExchangePoller({
166-
zcapClient,
167-
capability,
168-
filterExchange({exchange/*, previousPollResult*/}) {
169-
// ensure `accountId` matches exchange variables
170-
if(exchange?.variables.accountId !== accountId) {
171-
throw new BedrockError('Not authorized.', {
172-
name: 'NotAllowedError',
173-
details: {httpStatusCode: 403, public: true}
174-
});
175-
}
176-
// return only information that should be accessible to client
177-
return {
178-
exchange
179-
// FIXME: filter info once final step name and info is determined
180-
/*
181-
exchange: {
182-
state: exchange.state,
183-
result: exchange.variables.results?.finish
184-
}*/
185-
};
186-
}
187-
}),
188-
// set a TTL of 1 seconds to account for the case where a push
189-
// notification isn't received by the same instance that the client
190-
// hits, but prevent requests from triggering a hit to the backend more
191-
// frequently than 1 second
192-
ttl: 1000
179+
poller: _createExchangePoller({accountId, capability}),
180+
ttl: POLL_TTL
193181
});
194182

195183
res.json(result);
196184
}));
185+
186+
// push event handler
187+
app.post(
188+
routes.callback,
189+
push.createVerifyPushTokenMiddleware({event: 'exchangeUpdated'}),
190+
asyncHandler(async (req, res) => {
191+
const {event: {data: {exchangeId: id}}} = req.body;
192+
const {localInteractionId} = req.params;
193+
194+
// get interaction definition
195+
const definition = _getInteractionDefinition({localInteractionId});
196+
197+
// get capability for fetching exchange and verify its invocation target
198+
// matches the exchange ID passed
199+
const capability = definition.zcaps.get('readWriteExchanges');
200+
if(!id.startsWith(capability.invocationTarget)) {
201+
throw new BedrockError('Not authorized.', {
202+
name: 'NotAllowedError',
203+
details: {httpStatusCode: 403, public: true}
204+
});
205+
}
206+
207+
// poll (and clear cache)
208+
await poll({
209+
id,
210+
poller: _createExchangePoller({capability}),
211+
ttl: POLL_TTL,
212+
useCache: false
213+
});
214+
res.sendStatus(204);
215+
}));
197216
});
217+
218+
function _createExchangePoller({accountId, capability}) {
219+
return pollers.createExchangePoller({
220+
zcapClient,
221+
capability,
222+
filterExchange({exchange/*, previousPollResult*/}) {
223+
// if `accountId` given, ensure it matches exchange variables
224+
if(accountId && exchange?.variables.accountId !== accountId) {
225+
throw new BedrockError('Not authorized.', {
226+
name: 'NotAllowedError',
227+
details: {httpStatusCode: 403, public: true}
228+
});
229+
}
230+
// return only information that should be accessible to client
231+
return {
232+
exchange
233+
// FIXME: filter info once final step name and info is determined
234+
/*
235+
exchange: {
236+
state: exchange.state,
237+
result: exchange.variables.results?.finish
238+
}*/
239+
};
240+
}
241+
});
242+
}
243+
244+
function _getInteractionDefinition({localInteractionId}) {
245+
const definition = DEFINITIONS_BY_ID_MAP.get(localInteractionId);
246+
if(!definition) {
247+
throw new BedrockError(
248+
`Interaction type for "${localInteractionId}" not found.`, {
249+
name: 'NotFoundError',
250+
details: {httpStatusCode: 404, public: true}
251+
});
252+
}
253+
return definition;
254+
}

test/test.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,8 @@ config['profile-http'].interactions.types.test = {
4646
readWriteExchanges: '{}'
4747
}
4848
};
49+
// test hmac key for push token feature; required for `interactions`
50+
config.notify.push.hmacKey = {
51+
id: 'urn:test:hmacKey',
52+
secretKeyMultibase: 'uogHy02QDNPX4GID7dGUSGuYQ_Gv0WOIcpmTuKgt1ZNz7_4'
53+
};

0 commit comments

Comments
 (0)