@forge/realtime 0.1.1 → 0.2.0-experimental-3ed5db1

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.
@@ -1,203 +0,0 @@
1
- import * as runtime from '../runtime';
2
- import { publish, publishGlobal } from '../publish';
3
- import { FORGE_RUNTIME } from './utils';
4
-
5
- const TEST_EVENT_ID = 'event-id';
6
- const TEST_EVENT_TIMESTAMP = '1234567890';
7
- const MOCK_CHANNEL = 'my-channel';
8
- const MOCK_PUBLISH_OPTIONS = { token: 'my-token' };
9
- const MOCK_EVENT_PAYLOAD_STRING = 'this is an event payload';
10
- const MOCK_EVENT_PAYLOAD_OBJECT = { value: 'this is a test payload', funLevel: 100 };
11
-
12
- const MOCK_FETCH_RESPONSE = {
13
- errors: [],
14
- data: {
15
- ecosystem: {
16
- publishRealtimeChannel: {
17
- eventId: TEST_EVENT_ID,
18
- eventTimestamp: TEST_EVENT_TIMESTAMP
19
- },
20
- success: true
21
- }
22
- }
23
- }
24
-
25
- const MOCK_ERRORS = [
26
- {
27
- message: 'Error message',
28
- extensions: {
29
- errorType: 'Error type',
30
- statusCode: 500
31
- }
32
- }
33
- ];
34
-
35
- describe('publish', () => {
36
- beforeEach(() => {
37
- jest.restoreAllMocks();
38
- jest.resetAllMocks();
39
- });
40
-
41
- it('should publish an event with FCT in header', async () => {
42
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
43
-
44
- const mockForgeFetch = jest.fn().mockResolvedValue({
45
- json: jest.fn().mockResolvedValue(MOCK_FETCH_RESPONSE),
46
- status: 200,
47
- headers: { get: jest.fn().mockReturnValue(undefined) }
48
- });
49
-
50
- (global as any).__forge_fetch__ = mockForgeFetch;
51
-
52
- const response = await publish(MOCK_CHANNEL, MOCK_EVENT_PAYLOAD_STRING, MOCK_PUBLISH_OPTIONS);
53
-
54
- expect(response).toEqual({ eventId: TEST_EVENT_ID, eventTimestamp: TEST_EVENT_TIMESTAMP });
55
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
56
- });
57
-
58
- it('should publish an event when the payload is an object', async () => {
59
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
60
-
61
- const mockForgeFetch = jest.fn().mockResolvedValue({
62
- json: jest.fn().mockResolvedValue(MOCK_FETCH_RESPONSE),
63
- status: 200,
64
- headers: { get: jest.fn().mockReturnValue(undefined) }
65
- });
66
-
67
- (global as any).__forge_fetch__ = mockForgeFetch;
68
-
69
- const response = await publish(MOCK_CHANNEL, JSON.stringify(MOCK_EVENT_PAYLOAD_OBJECT), MOCK_PUBLISH_OPTIONS);
70
-
71
- expect(response).toEqual({ eventId: TEST_EVENT_ID, eventTimestamp: TEST_EVENT_TIMESTAMP });
72
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
73
- });
74
-
75
- it('should throw an error when Forge Outbound Proxy returns an error', async () => {
76
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
77
-
78
- const mockForgeFetch = jest.fn().mockResolvedValue({
79
- text: jest.fn().mockResolvedValue('Error occurred'),
80
- status: 502,
81
- headers: { get: jest.fn().mockReturnValue('UPSTREAM_FAILURE') }
82
- });
83
-
84
- (global as any).__forge_fetch__ = mockForgeFetch;
85
-
86
- const response = publish(MOCK_CHANNEL, JSON.stringify(MOCK_EVENT_PAYLOAD_OBJECT), MOCK_PUBLISH_OPTIONS);
87
-
88
- await expect(response).rejects.toThrow(
89
- 'Forge platform failed to process runtime HTTP request - 502 - UPSTREAM_FAILURE'
90
- );
91
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
92
- });
93
-
94
- it('should return null eventId and eventTimestamp when the response has errors', async () => {
95
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
96
-
97
- const mockForgeFetch = jest.fn().mockResolvedValue({
98
- json: jest.fn().mockResolvedValue({
99
- errors: MOCK_ERRORS,
100
- data: {
101
- ecosystem: {
102
- publishRealtimeChannel: null,
103
- }
104
- }
105
- }),
106
- status: 200,
107
- headers: { get: jest.fn().mockReturnValue(undefined) }
108
- });
109
-
110
- (global as any).__forge_fetch__ = mockForgeFetch;
111
-
112
- const payload = { value: 'this is a test payload', funLevel: 100 };
113
- const response = await publish('my-channel', JSON.stringify(payload));
114
-
115
- expect(response).toEqual({ eventId: null, eventTimestamp: null, errors: MOCK_ERRORS });
116
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
117
- });
118
- });
119
-
120
- describe('publishGlobal', () => {
121
- beforeEach(() => {
122
- jest.restoreAllMocks();
123
- jest.resetAllMocks();
124
- });
125
-
126
- it('should publish an event', async () => {
127
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
128
-
129
- const mockForgeFetch = jest.fn().mockResolvedValue({
130
- json: jest.fn().mockResolvedValue(MOCK_FETCH_RESPONSE),
131
- status: 200,
132
- headers: { get: jest.fn().mockReturnValue(undefined) }
133
- });
134
-
135
- (global as any).__forge_fetch__ = mockForgeFetch;
136
-
137
- const response = await publishGlobal(MOCK_CHANNEL, MOCK_EVENT_PAYLOAD_STRING, MOCK_PUBLISH_OPTIONS);
138
-
139
- expect(response).toEqual({ eventId: TEST_EVENT_ID, eventTimestamp: TEST_EVENT_TIMESTAMP });
140
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
141
- });
142
-
143
- it('should publish an event when the payload is an object', async () => {
144
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
145
-
146
- const mockForgeFetch = jest.fn().mockResolvedValue({
147
- json: jest.fn().mockResolvedValue(MOCK_FETCH_RESPONSE),
148
- status: 200,
149
- headers: { get: jest.fn().mockReturnValue(undefined) }
150
- });
151
-
152
- (global as any).__forge_fetch__ = mockForgeFetch;
153
-
154
- const response = await publishGlobal(MOCK_CHANNEL, JSON.stringify(MOCK_EVENT_PAYLOAD_OBJECT), MOCK_PUBLISH_OPTIONS);
155
-
156
- expect(response).toEqual({ eventId: TEST_EVENT_ID, eventTimestamp: TEST_EVENT_TIMESTAMP });
157
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
158
- });
159
-
160
- it('should throw an error when Forge Outbound Proxy returns an error', async () => {
161
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
162
-
163
- const mockForgeFetch = jest.fn().mockResolvedValue({
164
- text: jest.fn().mockResolvedValue('Error occurred'),
165
- status: 502,
166
- headers: { get: jest.fn().mockReturnValue('UPSTREAM_FAILURE') }
167
- });
168
-
169
- (global as any).__forge_fetch__ = mockForgeFetch;
170
-
171
- const response = publishGlobal(MOCK_CHANNEL, JSON.stringify(MOCK_EVENT_PAYLOAD_OBJECT), MOCK_PUBLISH_OPTIONS);
172
-
173
- await expect(response).rejects.toThrow(
174
- 'Forge platform failed to process runtime HTTP request - 502 - UPSTREAM_FAILURE'
175
- );
176
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
177
- });
178
-
179
- it('should return null eventId and eventTimestamp when the response has errors', async () => {
180
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
181
-
182
- const mockForgeFetch = jest.fn().mockResolvedValue({
183
- json: jest.fn().mockResolvedValue({
184
- errors: MOCK_ERRORS,
185
- data: {
186
- ecosystem: {
187
- publishRealtimeChannel: null,
188
- }
189
- }
190
- }),
191
- status: 200,
192
- headers: { get: jest.fn().mockReturnValue(undefined) }
193
- });
194
-
195
- (global as any).__forge_fetch__ = mockForgeFetch;
196
-
197
- const payload = { value: 'this is a test payload', funLevel: 100 };
198
- const response = await publishGlobal('my-channel', JSON.stringify(payload));
199
-
200
- expect(response).toEqual({ eventId: null, eventTimestamp: null, errors: MOCK_ERRORS });
201
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
202
- });
203
- });
@@ -1,13 +0,0 @@
1
- import { __getRuntime } from '../runtime';
2
- import { FORGE_RUNTIME } from './utils';
3
-
4
- describe('__getRuntime', () => {
5
- it('should return the runtime object', async () => {
6
- (global as any).__forge_runtime__ = FORGE_RUNTIME;
7
-
8
- const runtime = __getRuntime();
9
-
10
- expect(runtime.appContext.appId).toBe('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa');
11
- expect(runtime.contextAri).toBe('ari:cloud:jira::site/ffffffff-ffff-ffff-ffff-ffffffffffff');
12
- });
13
- });
@@ -1,96 +0,0 @@
1
- import * as runtime from '../runtime';
2
- import { signRealtimeToken } from '../signRealtimeToken';
3
- import { FORGE_RUNTIME } from './utils';
4
-
5
- const TEST_TOKEN = 'jwt-token';
6
- const TEST_EXPIRES_AT = '1234567890';
7
- const MOCK_ERRORS = [
8
- {
9
- message: 'Error message',
10
- extensions: {
11
- errorType: 'Error type',
12
- statusCode: 500
13
- }
14
- }
15
- ];
16
-
17
- describe('signRealtimeToken', () => {
18
- beforeEach(() => {
19
- jest.restoreAllMocks();
20
- jest.resetAllMocks();
21
- });
22
-
23
- it('should send a signRealtimeToken request and recieve a jwt and expiresAt', async () => {
24
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
25
-
26
- const mockForgeFetch = jest.fn().mockResolvedValue({
27
- json: jest.fn().mockResolvedValue({
28
- errors: [],
29
- data: {
30
- ecosystem: {
31
- signRealtimeToken: {
32
- errors: [],
33
- forgeRealtimeToken: {
34
- jwt: TEST_TOKEN,
35
- expiresAt: TEST_EXPIRES_AT
36
- },
37
- success: true
38
- }
39
- }
40
- }
41
- }),
42
- status: 200,
43
- headers: { get: jest.fn().mockReturnValue(undefined) }
44
- });
45
-
46
- (global as any).__forge_fetch__ = mockForgeFetch;
47
-
48
- const response = await signRealtimeToken('my-channel', { 'test-key': 'test-value' });
49
-
50
- expect(response).toEqual({ token: TEST_TOKEN, expiresAt: TEST_EXPIRES_AT });
51
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
52
- });
53
-
54
- it('should throw an error when Forge Outbound Proxy returns an error', async () => {
55
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
56
-
57
- const mockForgeFetch = jest.fn().mockResolvedValue({
58
- text: jest.fn().mockResolvedValue('Error occurred'),
59
- status: 502,
60
- headers: { get: jest.fn().mockReturnValue('UPSTREAM_FAILURE') }
61
- });
62
-
63
- (global as any).__forge_fetch__ = mockForgeFetch;
64
-
65
- const response = signRealtimeToken('my-channel', { 'test-key': 'test-value' });
66
-
67
- await expect(response).rejects.toThrow(
68
- 'Forge platform failed to process runtime HTTP request - 502 - UPSTREAM_FAILURE'
69
- );
70
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
71
- });
72
-
73
- it('should send generic error when errors are returned', async () => {
74
- jest.spyOn(runtime, '__getRuntime').mockReturnValue(FORGE_RUNTIME);
75
-
76
- const mockForgeFetch = jest.fn().mockResolvedValue({
77
- json: jest.fn().mockResolvedValue({
78
- errors: MOCK_ERRORS,
79
- data: {
80
- ecosystem: {
81
- signRealtimeToken: null,
82
- }
83
- }
84
- }),
85
- status: 200,
86
- headers: { get: jest.fn().mockReturnValue(undefined) }
87
- });
88
-
89
- (global as any).__forge_fetch__ = mockForgeFetch;
90
-
91
- const response = await signRealtimeToken('my-channel', { 'test-key': 'test-value' });
92
-
93
- expect(response).toEqual({ token: null, expiresAt: null, errors: MOCK_ERRORS });
94
- expect(mockForgeFetch.mock.calls).toMatchSnapshot();
95
- });
96
- });
@@ -1,33 +0,0 @@
1
- import type { ForgeRuntime } from '@forge/api';
2
- import { NoMetrics } from '@atlassian/metrics-interface';
3
-
4
- export const FORGE_RUNTIME: ForgeRuntime = {
5
- container: { runtime: 'node', region: 'us-west-2' },
6
- proxy: { url: 'https://foo', token: 'token' },
7
- contextAri: 'ari:cloud:jira::site/ffffffff-ffff-ffff-ffff-ffffffffffff',
8
- appContext: {
9
- appId: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
10
- environmentId: 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
11
- environmentType: 'DEVELOPMENT',
12
- invocationId: '33333',
13
- installationId: 'iiiiiiii-iiii-iiii-iiii-iiiiiiiiiiii',
14
- appVersion: '1.2.3',
15
- functionKey: 'functionKey',
16
- moduleType: 'moduleType',
17
- moduleKey: 'moduleKey',
18
- license: { isActive: true }
19
- },
20
- tracing: {
21
- traceId: 'traceId',
22
- spanId: 'spanId'
23
- },
24
- lambdaContext: {
25
- awsRequestId: '123',
26
- getRemainingTimeInMillis: jest.fn()
27
- },
28
- metrics: new NoMetrics(),
29
- realtime: {
30
- contextToken: 'my.context.token'
31
- },
32
- featureFlags: jest.fn()
33
- };
package/src/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export { publish, publishGlobal } from './publish';
2
- export { signRealtimeToken } from './signRealtimeToken';
package/src/publish.ts DELETED
@@ -1,160 +0,0 @@
1
- import { __getRuntime } from './runtime';
2
- import { handleProxyResponseErrors, prodEnvErrorMessage } from './utils';
3
-
4
- const graphqlBody = `mutation publishRealtimeChannel(
5
- $installationId: ID!
6
- $name: String!
7
- $payload: String!
8
- $isGlobal: Boolean
9
- $token: String
10
- ){
11
- ecosystem {
12
- publishRealtimeChannel(
13
- installationId: $installationId
14
- name: $name
15
- payload: $payload
16
- isGlobal: $isGlobal
17
- token: $token
18
- ) {
19
- eventId,
20
- eventTimestamp
21
- }
22
- }
23
- }`;
24
-
25
- interface PublishOptions {
26
- token?: string;
27
- }
28
-
29
- export const publish = async (channelName: string, eventPayload: string, options?: PublishOptions) => {
30
- const { appContext, realtime } = __getRuntime();
31
-
32
- if (appContext?.environmentType === 'PRODUCTION') {
33
- throw new Error(prodEnvErrorMessage);
34
- }
35
-
36
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
- const response = await (global as any).__forge_fetch__(
38
- {
39
- type: 'realtime'
40
- },
41
- '/',
42
- {
43
- method: 'POST',
44
- body: JSON.stringify({
45
- query: graphqlBody,
46
- variables: {
47
- installationId: appContext.installationId,
48
- name: channelName,
49
- payload: eventPayload,
50
- isGlobal: false,
51
- token: options?.token
52
- }
53
- }),
54
- errors: [],
55
- headers: {
56
- 'Content-Type': 'application/json',
57
- 'x-forge-context-token': realtime?.contextToken
58
- }
59
- }
60
- );
61
-
62
- handleProxyResponseErrors(response);
63
-
64
- const awaitedResponse = await response.json();
65
-
66
- const { data, errors } = awaitedResponse;
67
-
68
- if (errors && errors.length > 0) {
69
- return {
70
- eventId: null,
71
- eventTimestamp: null,
72
- errors
73
- };
74
- }
75
-
76
- if (!data) {
77
- return {
78
- eventId: null,
79
- eventTimestamp: null,
80
- errors: [
81
- {
82
- message: 'Error publishing event to channel.'
83
- }
84
- ]
85
- };
86
- }
87
-
88
- const { eventId, eventTimestamp } = awaitedResponse.data.ecosystem.publishRealtimeChannel;
89
-
90
- return {
91
- eventId,
92
- eventTimestamp
93
- };
94
- };
95
-
96
- export const publishGlobal = async (channelName: string, eventPayload: string, options?: PublishOptions) => {
97
- const { appContext } = __getRuntime();
98
-
99
- if (appContext?.environmentType === 'PRODUCTION') {
100
- throw new Error(prodEnvErrorMessage);
101
- }
102
-
103
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
- const response = await (global as any).__forge_fetch__(
105
- {
106
- type: 'realtime'
107
- },
108
- '/',
109
- {
110
- method: 'POST',
111
- body: JSON.stringify({
112
- query: graphqlBody,
113
- variables: {
114
- installationId: appContext.installationId,
115
- name: channelName,
116
- payload: eventPayload,
117
- isGlobal: true,
118
- token: options?.token
119
- }
120
- }),
121
- errors: [],
122
- headers: {
123
- 'Content-Type': 'application/json'
124
- }
125
- }
126
- );
127
-
128
- handleProxyResponseErrors(response);
129
-
130
- const awaitedResponse = await response.json();
131
-
132
- const { data, errors } = awaitedResponse;
133
-
134
- if (errors && errors.length > 0) {
135
- return {
136
- eventId: null,
137
- eventTimestamp: null,
138
- errors
139
- };
140
- }
141
-
142
- if (!data) {
143
- return {
144
- eventId: null,
145
- eventTimestamp: null,
146
- errors: [
147
- {
148
- message: 'Error publishing global event to channel.'
149
- }
150
- ]
151
- };
152
- }
153
-
154
- const { eventId, eventTimestamp } = data.ecosystem.publishRealtimeChannel;
155
-
156
- return {
157
- eventId,
158
- eventTimestamp
159
- };
160
- };
package/src/runtime.ts DELETED
@@ -1,9 +0,0 @@
1
- import type { ForgeRuntime } from '@forge/api';
2
-
3
- export function __getRuntime(): ForgeRuntime {
4
- const runtime = (global as any).__forge_runtime__;
5
- if (!runtime) {
6
- throw new Error('Forge runtime not found.');
7
- }
8
- return runtime as ForgeRuntime;
9
- }
@@ -1,90 +0,0 @@
1
- import { __getRuntime } from './runtime';
2
- import { handleProxyResponseErrors, prodEnvErrorMessage } from './utils';
3
-
4
- const graphqlBody = `mutation signRealtimeToken(
5
- $channelName: String!
6
- $claims: JSON!
7
- ){
8
- ecosystem {
9
- signRealtimeToken(
10
- channelName: $channelName
11
- claims: $claims
12
- ) {
13
- errors {
14
- message
15
- extensions {
16
- errorType
17
- statusCode
18
- }
19
- }
20
- forgeRealtimeToken {
21
- jwt
22
- expiresAt
23
- }
24
- success
25
- }
26
- }
27
- }`;
28
-
29
- export const signRealtimeToken = async (channelName: string, claims: any) => {
30
- const { appContext } = __getRuntime();
31
-
32
- if (appContext?.environmentType === 'PRODUCTION') {
33
- throw new Error(prodEnvErrorMessage);
34
- }
35
-
36
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
- const response = await (global as any).__forge_fetch__(
38
- {
39
- type: 'realtime'
40
- },
41
- '/',
42
- {
43
- method: 'POST',
44
- body: JSON.stringify({
45
- query: graphqlBody,
46
- variables: {
47
- channelName,
48
- claims
49
- }
50
- }),
51
- errors: [],
52
- headers: {
53
- 'Content-Type': 'application/json'
54
- }
55
- }
56
- );
57
-
58
- handleProxyResponseErrors(response);
59
-
60
- const awaitedResponse = await response.json();
61
-
62
- const { data, errors } = awaitedResponse;
63
-
64
- if (errors && errors.length > 0) {
65
- return {
66
- token: null,
67
- expiresAt: null,
68
- errors
69
- };
70
- }
71
-
72
- if (!data) {
73
- return {
74
- token: null,
75
- expiresAt: null,
76
- errors: [
77
- {
78
- message: 'Error signing realtime token.'
79
- }
80
- ]
81
- };
82
- }
83
-
84
- const { jwt, expiresAt } = data.ecosystem.signRealtimeToken.forgeRealtimeToken;
85
-
86
- return {
87
- token: jwt,
88
- expiresAt
89
- };
90
- };
package/src/utils.ts DELETED
@@ -1,12 +0,0 @@
1
- import { ProxyRequestError } from '@forge/api';
2
-
3
- const getForgeProxyError = (response: Response) => response.headers.get('forge-proxy-error');
4
- export const handleProxyResponseErrors = (response: Response): void => {
5
- const errorReason = getForgeProxyError(response);
6
- if (errorReason) {
7
- throw new ProxyRequestError(response.status, errorReason);
8
- }
9
- };
10
-
11
- export const prodEnvErrorMessage =
12
- 'Forge realtime usage is restricted to Forge apps in a non-production environment. Please see https://developer.atlassian.com/platform/forge/cli-reference/environments/ for reference on Forge app environments.';
package/tsconfig.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "extends": "../../tsconfig-base.json",
3
- "compilerOptions": {
4
- "lib": [
5
- "es2017",
6
- "DOM"
7
- ],
8
- "outDir": "./out",
9
- "rootDir": "src",
10
- "composite": true,
11
- "moduleResolution": "node"
12
- },
13
- "references": [
14
- {
15
- "path": "../forge-api"
16
- }
17
- ]
18
- }