anear-js-api 1.0.1 → 1.1.2

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/jest.config.js CHANGED
@@ -1,4 +1,10 @@
1
1
  require('dotenv').config({ path: '.env.test' })
2
+
3
+ // Provide sane defaults so unit tests don't depend on local machine env.
4
+ process.env.ANEARAPP_API_KEY = process.env.ANEARAPP_API_KEY || 'test-api-key'
5
+ process.env.ANEARAPP_API_SECRET = process.env.ANEARAPP_API_SECRET || 'test-api-secret'
6
+ process.env.ANEARAPP_API_VERSION = process.env.ANEARAPP_API_VERSION || 'v1'
7
+
2
8
  module.exports = {
3
9
  }
4
10
 
@@ -7,6 +7,6 @@ const AnearService = (appEventMachineFactory, appParticipantMachineFactory = nul
7
7
  // appEventMachineFactory = anearEvent => { returns XState Machine) }
8
8
  // optional appParticipantMachineFactory = anearParticipant => { returns XState Machine) }
9
9
  //
10
- AnearCoreServiceMachine(appEventMachineFactory, appParticipantMachineFactory)
10
+ return AnearCoreServiceMachine(appEventMachineFactory, appParticipantMachineFactory)
11
11
  }
12
12
  module.exports = AnearService
@@ -3,8 +3,8 @@ const logger = require('../utils/Logger')
3
3
  const ApiService = require('./ApiService')
4
4
 
5
5
  class AnearApi extends ApiService {
6
- constructor(apiKey, apiVersion) {
7
- super(apiKey, apiVersion)
6
+ constructor(apiKey, apiSecret, apiVersion) {
7
+ super(apiKey, apiSecret, apiVersion)
8
8
  }
9
9
 
10
10
  getAccount() {
@@ -105,12 +105,13 @@ class AnearApi extends ApiService {
105
105
 
106
106
  // Instantiate and export the API immediately
107
107
  const apiKey = process.env.ANEARAPP_API_KEY
108
+ const apiSecret = process.env.ANEARAPP_API_SECRET
108
109
  const apiVersion = process.env.ANEARAPP_API_VERSION
109
110
 
110
- if (!apiKey || !apiVersion) {
111
- throw new Error("API_KEY and API_VERSION must be defined in environment variables")
111
+ if (!apiKey || !apiSecret || !apiVersion) {
112
+ throw new Error("ANEARAPP_API_KEY, ANEARAPP_API_SECRET, and ANEARAPP_API_VERSION must be defined in environment variables")
112
113
  }
113
114
 
114
- const anearApiInstance = new AnearApi(apiKey, apiVersion)
115
+ const anearApiInstance = new AnearApi(apiKey, apiSecret, apiVersion)
115
116
 
116
117
  module.exports = anearApiInstance
@@ -13,19 +13,81 @@ const logger = require('../utils/Logger')
13
13
  const DEFAULT_DEVELOPER_API_URL = 'https://api.anear.me/developer'
14
14
 
15
15
  class ApiService {
16
- constructor(apiKey, apiVersion) {
16
+ constructor(apiKey, apiSecret, apiVersion) {
17
17
  const baseUrl = (process.env.ANEARAPP_API_URL || DEFAULT_DEVELOPER_API_URL).replace(/\/+$/, '')
18
18
  const versionSegment = apiVersion ? `/${apiVersion}` : ''
19
19
  this.api_base_url = `${baseUrl}${versionSegment}`
20
20
  logger.debug(`ApiService configured api_base_url=${this.api_base_url}`)
21
+
22
+ this.api_key = apiKey
23
+ this.api_secret = apiSecret
24
+ this.developer_auth = null
25
+ this._authPromise = null
26
+
21
27
  this.defaultHeaderObject = {
22
28
  'Accept': 'application/json',
23
- 'Content-Type': 'application/json',
24
- 'X-Api-Key': apiKey
29
+ 'Content-Type': 'application/json'
25
30
  }
26
31
  this.default_headers = new fetch.Headers(this.defaultHeaderObject)
27
32
  }
28
33
 
34
+ async ensureDeveloperAuth() {
35
+ const now = Math.floor(Date.now() / 1000)
36
+ if (this.developer_auth && this.developer_auth.expires_at && this.developer_auth.expires_at > now) {
37
+ return this.developer_auth
38
+ }
39
+
40
+ if (this._authPromise) return this._authPromise
41
+
42
+ this._authPromise = this.loginDeveloper()
43
+ .finally(() => {
44
+ this._authPromise = null
45
+ })
46
+
47
+ return this._authPromise
48
+ }
49
+
50
+ async loginDeveloper() {
51
+ if (!this.api_key || !this.api_secret) {
52
+ throw new Error("Developer API credentials must be provided (api_key + secret)")
53
+ }
54
+
55
+ const payload = {
56
+ data: {
57
+ type: "sessions",
58
+ attributes: {
59
+ api_key: this.api_key,
60
+ secret: this.api_secret
61
+ }
62
+ }
63
+ }
64
+
65
+ const request = new fetch.Request(
66
+ `${this.api_base_url}/sessions`, {
67
+ method: 'POST',
68
+ headers: this.default_headers,
69
+ body: JSON.stringify(payload)
70
+ }
71
+ )
72
+
73
+ logger.debug(`HTTP ${request.method} ${request.url} (developer session)`)
74
+ const resp = await fetch(request)
75
+ logger.debug(`HTTP response status=${resp.status} url=${request.url}`)
76
+ const json = await this.checkStatus(resp)
77
+
78
+ this.developer_auth = {
79
+ auth_token: json.auth_token,
80
+ expires_at: json.expires_at,
81
+ developer_id: json.developer_id
82
+ }
83
+
84
+ const bearer = `Bearer ${this.developer_auth.auth_token}`
85
+ this.default_headers.set('Authorization', bearer)
86
+ // Keep the plain-object form in sync for consumers like Ably authHeaders.
87
+ this.defaultHeaderObject['Authorization'] = bearer
88
+ return this.developer_auth
89
+ }
90
+
29
91
  idParamUrlString(resource, params) {
30
92
  let idParam = ''
31
93
  if (params.id) {
@@ -103,6 +165,11 @@ class ApiService {
103
165
  }
104
166
 
105
167
  async issueRequest(request) {
168
+ await this.ensureDeveloperAuth()
169
+ // Ensure each request has the latest Authorization header
170
+ if (this.developer_auth && this.developer_auth.auth_token) {
171
+ request.headers.set('Authorization', `Bearer ${this.developer_auth.auth_token}`)
172
+ }
106
173
  logger.debug(`HTTP ${request.method} ${request.url}`)
107
174
  const resp = await fetch(request)
108
175
  logger.debug(`HTTP response status=${resp.status} url=${request.url}`)
@@ -158,12 +158,15 @@ class AnearEvent extends JsonApiResource {
158
158
  return this.attributes.flags.includes(flagName)
159
159
  }
160
160
 
161
- allowsSpectators() {
161
+ spectatorsAllowed() {
162
162
  //
163
163
  // Canonical spectators decision uses the ANAPI-provided boolean when present.
164
164
  // Fallback to legacy negative flag for older payloads.
165
165
  //
166
- return this.attributes['spectators-allowed']
166
+ const allowedDasherized = this.attributes['spectators-allowed']
167
+ if (typeof allowedDasherized === 'boolean') return allowedDasherized
168
+
169
+ return !this.hasFlag('no_spectators')
167
170
  }
168
171
 
169
172
  isParticipantEventCreator(participant) {
@@ -1850,8 +1850,8 @@ const AnearEventMachineFunctions = ({
1850
1850
  eventSupportsSpectators: ({ context }) => {
1851
1851
  try {
1852
1852
  // True only when ANAPI has indicated spectators are allowed for this event.
1853
- // JSAPI AnearEvent#allowsSpectators reads the canonical spectators-allowed flag.
1854
- return !!(context.anearEvent && context.anearEvent.allowsSpectators && context.anearEvent.allowsSpectators())
1853
+ // JSAPI AnearEvent#spectatorsAllowed reads the canonical spectators-allowed flag.
1854
+ return !!(context.anearEvent && context.anearEvent.spectatorsAllowed && context.anearEvent.spectatorsAllowed())
1855
1855
  } catch (_e) {
1856
1856
  return false
1857
1857
  }
@@ -234,7 +234,10 @@ const AnearParticipantMachineConfig = participantId => ({
234
234
  input: ({ context, event }) => ({ context, event }),
235
235
  onDone: [
236
236
  { guard: 'hasActionTimeout', actions: 'updateActionTimeout', target: 'waitParticipantResponse' },
237
- { target: 'idle' }
237
+ // If this display does not configure a timeout, explicitly clear any
238
+ // previously-running action timeout. Otherwise, a stale actionTimeoutStart
239
+ // from an older prompt can be "resumed" later and immediately fire.
240
+ { actions: 'nullActionTimeout', target: 'idle' }
238
241
  ],
239
242
  onError: {
240
243
  target: '#error'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anear-js-api",
3
- "version": "1.0.1",
3
+ "version": "1.1.2",
4
4
  "description": "Javascript Developer API for Anear Apps",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -29,7 +29,7 @@ describe('AnearEvent', () => {
29
29
  const mockService = { send: jest.fn() }
30
30
  anearEvent.setMachine(mockService)
31
31
  anearEvent.announceEvent()
32
- expect(mockService.send).toHaveBeenCalledWith('ANNOUNCE')
32
+ expect(mockService.send).toHaveBeenCalledWith({ type: 'ANNOUNCE' })
33
33
  })
34
34
 
35
35
  test('can be hosted', () => {
@@ -46,8 +46,8 @@ describe('AnearEvent', () => {
46
46
  expect(anearEvent.hasFlag('no_spectators')).toBe(true)
47
47
  })
48
48
 
49
- test('allowsSpectators is false when no_spectators flag is present', () => {
50
- expect(anearEvent.allowsSpectators()).toBe(false)
49
+ test('spectatorsAllowed is false when no_spectators flag is present', () => {
50
+ expect(anearEvent.spectatorsAllowed()).toBe(false)
51
51
  })
52
52
 
53
53
  test('isPlayable returns true for playable states', () => {
@@ -94,32 +94,27 @@ describe('AnearEvent', () => {
94
94
 
95
95
  test('announceEvent sends ANNOUNCE', () => {
96
96
  anearEvent.announceEvent()
97
- expect(mockService.send).toHaveBeenCalledWith('ANNOUNCE')
97
+ expect(mockService.send).toHaveBeenCalledWith({ type: 'ANNOUNCE' })
98
98
  })
99
99
 
100
100
  test('startEvent sends START', () => {
101
101
  anearEvent.startEvent()
102
- expect(mockService.send).toHaveBeenCalledWith('START')
102
+ expect(mockService.send).toHaveBeenCalledWith({ type: 'START' })
103
103
  })
104
104
 
105
105
  test('cancelEvent sends CANCEL', () => {
106
106
  anearEvent.cancelEvent()
107
- expect(mockService.send).toHaveBeenCalledWith('CANCEL')
107
+ expect(mockService.send).toHaveBeenCalledWith({ type: 'CANCEL' })
108
108
  })
109
109
 
110
110
  test('closeEvent sends CLOSE', () => {
111
111
  anearEvent.closeEvent()
112
- expect(mockService.send).toHaveBeenCalledWith('CLOSE')
112
+ expect(mockService.send).toHaveBeenCalledWith({ type: 'CLOSE' })
113
113
  })
114
114
 
115
115
  test('pauseEvent sends PAUSE', () => {
116
116
  anearEvent.pauseEvent()
117
- expect(mockService.send).toHaveBeenCalledWith('PAUSE')
118
- })
119
-
120
- test('resumeEvent sends RESUME', () => {
121
- anearEvent.resumeEvent()
122
- expect(mockService.send).toHaveBeenCalledWith('RESUME')
117
+ expect(mockService.send).toHaveBeenCalledWith({ type: 'PAUSE', appmContext: { context: undefined, resumeEvent: { type: 'RESUME' } } })
123
118
  })
124
119
  })
125
120
  })
@@ -43,12 +43,12 @@ describe('AnearParticipant', () => {
43
43
  })
44
44
 
45
45
  test('isHost returns false for participants', () => {
46
- expect(participant.isHost()).toBe(false)
46
+ expect(participant.isHost).toBe(false)
47
47
  })
48
48
 
49
49
  test('isHost returns true for hosts', () => {
50
50
  participant.attributes['user-type'] = 'host'
51
- expect(participant.isHost()).toBe(true)
51
+ expect(participant.isHost).toBe(true)
52
52
  })
53
53
 
54
54
  test('eventId returns the correct event ID', () => {
@@ -12,5 +12,6 @@ const MockParticipantClass = class ParticipantMachine {}
12
12
  test('test happy path', () => {
13
13
  const mockEventMachineFactory = (anearEvent) => new MockAppClass(anearEvent)
14
14
  const mockParticipantMachineFactory = (anearParticipant) => new MockParticipantClass(anearParticipant)
15
- AnearService(mockEventMachineFactory, mockParticipantMachineFactory)
15
+ const service = AnearService(mockEventMachineFactory, mockParticipantMachineFactory)
16
+ service.stop()
16
17
  })
@@ -74,10 +74,10 @@ describe('RealtimeMessaging', () => {
74
74
  const stateChangeCallback = mockRealtimeInstance.connection.on.mock.calls[0][0]
75
75
 
76
76
  stateChangeCallback({ current: 'connected' })
77
- expect(mockActor.send).toHaveBeenCalledWith('CONNECTED')
77
+ expect(mockActor.send).toHaveBeenCalledWith({ type: 'CONNECTED' })
78
78
 
79
79
  stateChangeCallback({ current: 'suspended' })
80
- expect(mockActor.send).toHaveBeenCalledWith('SUSPENDED')
80
+ expect(mockActor.send).toHaveBeenCalledWith({ type: 'SUSPENDED' })
81
81
  })
82
82
  })
83
83
 
@@ -105,10 +105,10 @@ describe('RealtimeMessaging', () => {
105
105
  const stateChangeCallback = mockChannel.on.mock.calls[0][1]
106
106
 
107
107
  stateChangeCallback({ current: 'attached' })
108
- expect(mockActor.send).toHaveBeenCalledWith('ATTACHED', { actor: mockActor })
108
+ expect(mockActor.send).toHaveBeenCalledWith({ type: 'ATTACHED', data: { channelName: 'mock-channel' } })
109
109
 
110
110
  stateChangeCallback({ current: 'failed' })
111
- expect(mockActor.send).toHaveBeenCalledWith('FAILED', { actor: mockActor })
111
+ expect(mockActor.send).toHaveBeenCalledWith({ type: 'FAILED', data: { channelName: 'mock-channel' } })
112
112
  })
113
113
  })
114
114
 
@@ -132,7 +132,7 @@ describe('RealtimeMessaging', () => {
132
132
  const message = { data: { id: 'user-1' } }
133
133
  presenceCallback(message)
134
134
 
135
- expect(mockActor.send).toHaveBeenCalledWith('PARTICIPANT_ENTER', { data: message.data })
135
+ expect(mockActor.send).toHaveBeenCalledWith({ type: 'PARTICIPANT_ENTER', data: message.data })
136
136
  })
137
137
  })
138
138
 
@@ -193,7 +193,7 @@ describe('RealtimeMessaging', () => {
193
193
  const messageCallback = mockChannel.subscribe.mock.calls[0][1]
194
194
  const message = { name: 'incoming-event', data: { info: 'abc' } }
195
195
  messageCallback(message)
196
- expect(mockActor.send).toHaveBeenCalledWith(message.name, { data: message.data })
196
+ expect(mockActor.send).toHaveBeenCalledWith({ type: message.name, data: message.data })
197
197
  })
198
198
  })
199
199