@forwardimpact/libbridge 0.1.3 → 0.1.4
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/package.json +3 -2
- package/src/dispatcher.js +17 -33
- package/src/index.js +1 -0
- package/src/resume-scheduler.js +51 -72
- package/src/token-resolver.js +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libbridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Threaded-channel bridge primitives — relay messages between human channels (GitHub Discussions, Microsoft Teams) and the Kata agent team.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bridge",
|
|
@@ -42,11 +42,12 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@forwardimpact/libindex": "^0.1.38",
|
|
44
44
|
"@forwardimpact/libstorage": "^0.1.78",
|
|
45
|
+
"@forwardimpact/libtype": "^0.1.0",
|
|
45
46
|
"@hono/node-server": "^2.0.4",
|
|
46
47
|
"hono": "^4.12.23"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
|
-
"@forwardimpact/
|
|
50
|
+
"@forwardimpact/libmock": "^0.1.0"
|
|
50
51
|
},
|
|
51
52
|
"engines": {
|
|
52
53
|
"bun": ">=1.2.0",
|
package/src/dispatcher.js
CHANGED
|
@@ -3,27 +3,7 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { dispatchWorkflow } from "./dispatch.js";
|
|
4
4
|
import { appendHistory } from "./history.js";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* The standard "dispatch dance" both bridges perform: generate a
|
|
8
|
-
* correlation ID, register the callback token, start the acknowledgement
|
|
9
|
-
* (if `ackTarget` is supplied), fire the kata-dispatch workflow, append
|
|
10
|
-
* history, push the dispatch timestamp, and flush the store. On failure
|
|
11
|
-
* the acknowledgement is finished and the callback registration is rolled
|
|
12
|
-
* back before the error rethrows.
|
|
13
|
-
*
|
|
14
|
-
* The caller still owns: loading/creating the context, checking the rate
|
|
15
|
-
* limiter, and deciding the user-facing action when `dispatch()` throws.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* const { token, correlationId } = await dispatcher.dispatch({
|
|
19
|
-
* ctx,
|
|
20
|
-
* prompt,
|
|
21
|
-
* ackTarget: { subjectId: nodeId },
|
|
22
|
-
* historyText: text,
|
|
23
|
-
* callbackMeta: { discussionId },
|
|
24
|
-
* workflowInputs: { discussionId },
|
|
25
|
-
* });
|
|
26
|
-
*/
|
|
6
|
+
/** Dispatch dance: resolve per-user token, register callback, ack, fire workflow, flush. */
|
|
27
7
|
export class Dispatcher {
|
|
28
8
|
#callbacks;
|
|
29
9
|
#ack;
|
|
@@ -31,7 +11,7 @@ export class Dispatcher {
|
|
|
31
11
|
#callbackBaseUrl;
|
|
32
12
|
#workflowFile;
|
|
33
13
|
#githubRepo;
|
|
34
|
-
#
|
|
14
|
+
#tokenResolver;
|
|
35
15
|
|
|
36
16
|
/**
|
|
37
17
|
* @param {object} options
|
|
@@ -41,7 +21,7 @@ export class Dispatcher {
|
|
|
41
21
|
* @param {string} options.callbackBaseUrl - Already normalised
|
|
42
22
|
* @param {string} options.workflowFile
|
|
43
23
|
* @param {string} options.githubRepo
|
|
44
|
-
* @param {()
|
|
24
|
+
* @param {import("./token-resolver.js").TokenResolver} options.tokenResolver
|
|
45
25
|
*/
|
|
46
26
|
constructor({
|
|
47
27
|
callbacks,
|
|
@@ -50,7 +30,7 @@ export class Dispatcher {
|
|
|
50
30
|
callbackBaseUrl,
|
|
51
31
|
workflowFile,
|
|
52
32
|
githubRepo,
|
|
53
|
-
|
|
33
|
+
tokenResolver,
|
|
54
34
|
}) {
|
|
55
35
|
if (!callbacks) throw new Error("callbacks is required");
|
|
56
36
|
if (!ack) throw new Error("ack is required");
|
|
@@ -60,31 +40,31 @@ export class Dispatcher {
|
|
|
60
40
|
}
|
|
61
41
|
if (!workflowFile) throw new Error("workflowFile is required");
|
|
62
42
|
if (!githubRepo) throw new Error("githubRepo is required");
|
|
63
|
-
if (
|
|
64
|
-
throw new Error("getGithubToken is required");
|
|
65
|
-
}
|
|
43
|
+
if (!tokenResolver) throw new Error("tokenResolver is required");
|
|
66
44
|
this.#callbacks = callbacks;
|
|
67
45
|
this.#ack = ack;
|
|
68
46
|
this.#store = store;
|
|
69
47
|
this.#callbackBaseUrl = callbackBaseUrl;
|
|
70
48
|
this.#workflowFile = workflowFile;
|
|
71
49
|
this.#githubRepo = githubRepo;
|
|
72
|
-
this.#
|
|
50
|
+
this.#tokenResolver = tokenResolver;
|
|
73
51
|
}
|
|
74
52
|
|
|
75
53
|
/**
|
|
76
54
|
* @param {object} args
|
|
77
55
|
* @param {object} args.ctx - Discussion context record (mutated)
|
|
78
56
|
* @param {string} args.prompt
|
|
57
|
+
* @param {string} args.requester - Surface user id of the triggering human
|
|
79
58
|
* @param {object} args.callbackMeta - Stored on the callback token
|
|
80
59
|
* @param {unknown} [args.ackTarget] - If omitted, no acknowledgement is started
|
|
81
60
|
* @param {string} [args.historyText] - Appended to ctx.history as the user turn on success
|
|
82
61
|
* @param {object} [args.workflowInputs] - Extra fields for `dispatchWorkflow`
|
|
83
|
-
* @returns {Promise<{token: string, correlationId: string}>}
|
|
62
|
+
* @returns {Promise<{kind: "dispatched", token: string, correlationId: string} | {kind: "link_required", authorizeUrl: string} | {kind: "reauth_required"} | {kind: "transient", error: Error}>}
|
|
84
63
|
*/
|
|
85
64
|
async dispatch({
|
|
86
65
|
ctx,
|
|
87
66
|
prompt,
|
|
67
|
+
requester,
|
|
88
68
|
callbackMeta,
|
|
89
69
|
ackTarget,
|
|
90
70
|
historyText,
|
|
@@ -92,19 +72,23 @@ export class Dispatcher {
|
|
|
92
72
|
}) {
|
|
93
73
|
if (!ctx) throw new Error("ctx is required");
|
|
94
74
|
if (typeof prompt !== "string") throw new Error("prompt is required");
|
|
75
|
+
if (typeof requester !== "string") throw new Error("requester is required");
|
|
76
|
+
|
|
77
|
+
const auth = await this.#tokenResolver.resolve(ctx.channel, requester);
|
|
78
|
+
if (auth.kind !== "token") return auth;
|
|
95
79
|
|
|
96
80
|
const correlationId = randomUUID();
|
|
97
|
-
const
|
|
81
|
+
const mergedMeta = { ...(callbackMeta ?? {}), requester };
|
|
82
|
+
const token = this.#callbacks.register(correlationId, mergedMeta);
|
|
98
83
|
ctx.pending_callbacks[token] = correlationId;
|
|
99
84
|
const callbackUrl = `${this.#callbackBaseUrl}/api/callback/${token}`;
|
|
100
85
|
|
|
101
86
|
if (ackTarget !== undefined) await this.#ack.start(token, ackTarget);
|
|
102
87
|
try {
|
|
103
|
-
const ghToken = await this.#getGithubToken();
|
|
104
88
|
await dispatchWorkflow({
|
|
105
89
|
workflowFile: this.#workflowFile,
|
|
106
90
|
repo: this.#githubRepo,
|
|
107
|
-
token:
|
|
91
|
+
token: auth.token,
|
|
108
92
|
prompt,
|
|
109
93
|
callbackUrl,
|
|
110
94
|
correlationId,
|
|
@@ -117,7 +101,7 @@ export class Dispatcher {
|
|
|
117
101
|
ctx.last_active_at = Date.now();
|
|
118
102
|
await this.#store.add(ctx);
|
|
119
103
|
await this.#store.flush();
|
|
120
|
-
return { token, correlationId };
|
|
104
|
+
return { kind: "dispatched", token, correlationId };
|
|
121
105
|
} catch (err) {
|
|
122
106
|
if (ackTarget !== undefined) await this.#ack.finish(token, ackTarget);
|
|
123
107
|
this.#callbacks.consume(token);
|
package/src/index.js
CHANGED
package/src/resume-scheduler.js
CHANGED
|
@@ -3,46 +3,7 @@ import { evaluateTrigger, parseIsoDuration } from "./triggers.js";
|
|
|
3
3
|
|
|
4
4
|
const DEFAULT_PROMPT = "Resume requested.";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Channel-agnostic suspend/resume lifecycle for the discuss-mode trace.
|
|
8
|
-
* When the workflow returns a `"recessed"` verdict, the bridge calls
|
|
9
|
-
* `enterRecess(...)` to persist the trigger on
|
|
10
|
-
* `ctx.open_rfcs[correlationId]`. The scheduler watches inbound activity
|
|
11
|
-
* via `processInbound(ctx)` and ticking elapsed timers via the embedded
|
|
12
|
-
* `ElapsedScheduler`; when a trigger fires, it redispatches the workflow
|
|
13
|
-
* through the shared `Dispatcher` with a `resume_context` payload
|
|
14
|
-
* linking back to the original correlation id.
|
|
15
|
-
*
|
|
16
|
-
* Channel-specific extras are supplied by two small constructor
|
|
17
|
-
* callbacks:
|
|
18
|
-
*
|
|
19
|
-
* buildCallbackMeta(ctx) -> { ... } // matches the bridge's loadDiscussionId
|
|
20
|
-
* buildResumeInputs(ctx) -> { ... } // extra workflow_dispatch inputs
|
|
21
|
-
*
|
|
22
|
-
* Both default to ghbridge's convention; msbridge overrides
|
|
23
|
-
* `buildCallbackMeta` to `{ threadId: ctx.discussion_id }` when it
|
|
24
|
-
* adopts resume.
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* const scheduler = new ResumeScheduler({
|
|
28
|
-
* dispatcher,
|
|
29
|
-
* store,
|
|
30
|
-
* logger,
|
|
31
|
-
* buildCallbackMeta: (ctx) => ({ discussionId: ctx.discussion_id }),
|
|
32
|
-
* buildResumeInputs: (ctx) => ({ discussionId: ctx.discussion_id }),
|
|
33
|
-
* });
|
|
34
|
-
* // service start:
|
|
35
|
-
* await scheduler.rearm();
|
|
36
|
-
* // inbound activity:
|
|
37
|
-
* const { freshDispatchAllowed } = await scheduler.processInbound(ctx);
|
|
38
|
-
* if (freshDispatchAllowed) { ... dispatch fresh ... }
|
|
39
|
-
* // on "recessed" verdict:
|
|
40
|
-
* scheduler.enterRecess(ctx, correlationId, payload.trigger);
|
|
41
|
-
* // on "adjourned" / "failed":
|
|
42
|
-
* scheduler.cancelRecess(ctx, correlationId);
|
|
43
|
-
* // service stop:
|
|
44
|
-
* scheduler.clear();
|
|
45
|
-
*/
|
|
6
|
+
/** Channel-agnostic suspend/resume lifecycle for the discuss-mode trace. */
|
|
46
7
|
export class ResumeScheduler {
|
|
47
8
|
#dispatcher;
|
|
48
9
|
#store;
|
|
@@ -51,6 +12,7 @@ export class ResumeScheduler {
|
|
|
51
12
|
#prompt;
|
|
52
13
|
#buildResumeInputs;
|
|
53
14
|
#buildCallbackMeta;
|
|
15
|
+
#onDeclined;
|
|
54
16
|
|
|
55
17
|
/**
|
|
56
18
|
* @param {object} options
|
|
@@ -60,6 +22,7 @@ export class ResumeScheduler {
|
|
|
60
22
|
* @param {string} [options.prompt] - Default "Resume requested."
|
|
61
23
|
* @param {(ctx: object) => object} [options.buildCallbackMeta]
|
|
62
24
|
* @param {(ctx: object) => object} [options.buildResumeInputs]
|
|
25
|
+
* @param {((ctx: object, outcome: object) => Promise<void>) | null} [options.onDeclined]
|
|
63
26
|
*/
|
|
64
27
|
constructor({
|
|
65
28
|
dispatcher,
|
|
@@ -68,6 +31,7 @@ export class ResumeScheduler {
|
|
|
68
31
|
prompt = DEFAULT_PROMPT,
|
|
69
32
|
buildCallbackMeta = (ctx) => ({ discussionId: ctx.discussion_id }),
|
|
70
33
|
buildResumeInputs = () => ({}),
|
|
34
|
+
onDeclined = null,
|
|
71
35
|
}) {
|
|
72
36
|
if (!dispatcher) throw new Error("dispatcher is required");
|
|
73
37
|
if (!store) throw new Error("store is required");
|
|
@@ -77,12 +41,16 @@ export class ResumeScheduler {
|
|
|
77
41
|
if (typeof buildResumeInputs !== "function") {
|
|
78
42
|
throw new Error("buildResumeInputs must be a function");
|
|
79
43
|
}
|
|
44
|
+
if (onDeclined != null && typeof onDeclined !== "function") {
|
|
45
|
+
throw new Error("onDeclined must be a function");
|
|
46
|
+
}
|
|
80
47
|
this.#dispatcher = dispatcher;
|
|
81
48
|
this.#store = store;
|
|
82
49
|
this.#logger = logger ?? null;
|
|
83
50
|
this.#prompt = prompt;
|
|
84
51
|
this.#buildCallbackMeta = buildCallbackMeta;
|
|
85
52
|
this.#buildResumeInputs = buildResumeInputs;
|
|
53
|
+
this.#onDeclined = onDeclined;
|
|
86
54
|
this.#elapsed = new ElapsedScheduler({
|
|
87
55
|
onFire: (cid) => this.#fireElapsed(cid),
|
|
88
56
|
onError: (err, cid) =>
|
|
@@ -99,10 +67,9 @@ export class ResumeScheduler {
|
|
|
99
67
|
|
|
100
68
|
/**
|
|
101
69
|
* Begin watching `correlationId` for trigger resolution. Persists the
|
|
102
|
-
* trigger
|
|
103
|
-
* `ctx.open_rfcs[correlationId]`. If the trigger has an
|
|
104
|
-
* component, schedules an in-memory timer
|
|
105
|
-
* inbound activity arrives. No-op if `trigger` is falsy.
|
|
70
|
+
* trigger, the history index at recess time, and the triggering
|
|
71
|
+
* requester onto `ctx.open_rfcs[correlationId]`. If the trigger has an
|
|
72
|
+
* elapsed component, schedules an in-memory timer.
|
|
106
73
|
*
|
|
107
74
|
* The caller is responsible for flushing the store after this call —
|
|
108
75
|
* `enterRecess` mutates ctx but does not write.
|
|
@@ -110,14 +77,16 @@ export class ResumeScheduler {
|
|
|
110
77
|
* @param {object} ctx
|
|
111
78
|
* @param {string} correlationId
|
|
112
79
|
* @param {import("./triggers.js").ResumeTrigger} trigger
|
|
80
|
+
* @param {string} requester - Surface user id of the triggering human
|
|
113
81
|
*/
|
|
114
|
-
enterRecess(ctx, correlationId, trigger) {
|
|
82
|
+
enterRecess(ctx, correlationId, trigger, requester) {
|
|
115
83
|
if (!trigger) return;
|
|
116
84
|
const openedAt = Date.now();
|
|
117
85
|
ctx.open_rfcs[correlationId] = {
|
|
118
86
|
trigger,
|
|
119
87
|
opened_at: openedAt,
|
|
120
88
|
history_index_at_open: ctx.history.length,
|
|
89
|
+
requester,
|
|
121
90
|
};
|
|
122
91
|
if (trigger.kind === "elapsed" && typeof trigger.elapsed === "string") {
|
|
123
92
|
const dueAt = openedAt + parseIsoDuration(trigger.elapsed);
|
|
@@ -128,8 +97,7 @@ export class ResumeScheduler {
|
|
|
128
97
|
|
|
129
98
|
/**
|
|
130
99
|
* Stop watching `correlationId`. Removes the rfc from `ctx.open_rfcs`
|
|
131
|
-
* and cancels any associated elapsed timer. Idempotent
|
|
132
|
-
* for verdicts that didn't actually recess.
|
|
100
|
+
* and cancels any associated elapsed timer. Idempotent.
|
|
133
101
|
*
|
|
134
102
|
* @param {object} ctx
|
|
135
103
|
* @param {string} correlationId
|
|
@@ -140,29 +108,21 @@ export class ResumeScheduler {
|
|
|
140
108
|
}
|
|
141
109
|
|
|
142
110
|
/**
|
|
143
|
-
* Walk `ctx.open_rfcs`. For each rfc whose trigger has fired
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* cancel the rfc. Returns a summary so the host can decide whether to
|
|
147
|
-
* additionally fire a fresh lead session on this inbound activity.
|
|
148
|
-
*
|
|
149
|
-
* If a redispatch fails the rfc remains armed — the host's failure
|
|
150
|
-
* recovery (next inbound activity, or the next elapsed tick) will
|
|
151
|
-
* retry.
|
|
111
|
+
* Walk `ctx.open_rfcs`. For each rfc whose trigger has fired,
|
|
112
|
+
* redispatch the workflow. Returns a summary so the host can decide
|
|
113
|
+
* whether to additionally fire a fresh lead session.
|
|
152
114
|
*
|
|
153
115
|
* @param {object} ctx
|
|
154
|
-
* @returns {Promise<{
|
|
155
|
-
* fired: number,
|
|
156
|
-
* hasOpenRfc: boolean,
|
|
157
|
-
* freshDispatchAllowed: boolean,
|
|
158
|
-
* }>}
|
|
116
|
+
* @returns {Promise<{fired: number, hasOpenRfc: boolean, freshDispatchAllowed: boolean}>}
|
|
159
117
|
*/
|
|
160
118
|
async processInbound(ctx) {
|
|
161
119
|
const fired = this.#evaluate(ctx);
|
|
162
120
|
for (const { correlationId, rfc } of fired) {
|
|
163
121
|
const historySince = ctx.history.slice(rfc.history_index_at_open);
|
|
164
|
-
await this.#redispatch(ctx, correlationId, historySince);
|
|
165
|
-
|
|
122
|
+
const result = await this.#redispatch(ctx, correlationId, historySince);
|
|
123
|
+
if (result.kind === "dispatched") {
|
|
124
|
+
this.cancelRecess(ctx, correlationId);
|
|
125
|
+
}
|
|
166
126
|
}
|
|
167
127
|
const hasOpenRfc = Object.keys(ctx.open_rfcs ?? {}).length > 0;
|
|
168
128
|
return {
|
|
@@ -173,9 +133,7 @@ export class ResumeScheduler {
|
|
|
173
133
|
}
|
|
174
134
|
|
|
175
135
|
/**
|
|
176
|
-
* Rehydrate persistent elapsed timers from the store.
|
|
177
|
-
* the bridge server starts so deadlines set before a restart still
|
|
178
|
-
* fire.
|
|
136
|
+
* Rehydrate persistent elapsed timers from the store.
|
|
179
137
|
* @returns {Promise<void>}
|
|
180
138
|
*/
|
|
181
139
|
async rearm() {
|
|
@@ -213,19 +171,38 @@ export class ResumeScheduler {
|
|
|
213
171
|
}
|
|
214
172
|
|
|
215
173
|
async #redispatch(ctx, correlationId, historySince) {
|
|
174
|
+
const rfc = ctx.open_rfcs[correlationId];
|
|
175
|
+
if (!rfc.requester) {
|
|
176
|
+
this.cancelRecess(ctx, correlationId);
|
|
177
|
+
this.#logger?.info?.("resume.skip", "rfc missing requester", {
|
|
178
|
+
correlation_id: correlationId,
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
kind: "transient",
|
|
182
|
+
error: new Error("rfc missing requester"),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
216
185
|
const resumeContext = JSON.stringify({
|
|
217
186
|
correlation_id: correlationId,
|
|
218
187
|
history_since: historySince,
|
|
219
188
|
});
|
|
220
|
-
await this.#dispatcher.dispatch({
|
|
189
|
+
const result = await this.#dispatcher.dispatch({
|
|
221
190
|
ctx,
|
|
222
191
|
prompt: this.#prompt,
|
|
192
|
+
requester: rfc.requester,
|
|
223
193
|
callbackMeta: this.#buildCallbackMeta(ctx),
|
|
224
194
|
workflowInputs: {
|
|
225
195
|
...this.#buildResumeInputs(ctx),
|
|
226
196
|
resumeContext,
|
|
227
197
|
},
|
|
228
198
|
});
|
|
199
|
+
if (result.kind !== "dispatched") {
|
|
200
|
+
this.cancelRecess(ctx, correlationId);
|
|
201
|
+
await this.#store.add(ctx);
|
|
202
|
+
await this.#store.flush();
|
|
203
|
+
if (this.#onDeclined) await this.#onDeclined(ctx, result);
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
229
206
|
}
|
|
230
207
|
|
|
231
208
|
async #fireElapsed(correlationId) {
|
|
@@ -233,11 +210,13 @@ export class ResumeScheduler {
|
|
|
233
210
|
if (!found) return;
|
|
234
211
|
const { ctx, rfc } = found;
|
|
235
212
|
const historySince = ctx.history.slice(rfc.history_index_at_open);
|
|
236
|
-
await this.#redispatch(ctx, correlationId, historySince);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
213
|
+
const result = await this.#redispatch(ctx, correlationId, historySince);
|
|
214
|
+
if (result.kind === "dispatched") {
|
|
215
|
+
this.cancelRecess(ctx, correlationId);
|
|
216
|
+
ctx.last_active_at = Date.now();
|
|
217
|
+
await this.#store.add(ctx);
|
|
218
|
+
await this.#store.flush();
|
|
219
|
+
}
|
|
241
220
|
}
|
|
242
221
|
|
|
243
222
|
async #findContextWithRfc(correlationId) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ghauth } from "@forwardimpact/libtype";
|
|
2
|
+
|
|
3
|
+
/** Maps ghauth GetToken oneof + gRPC transport into a discriminated DispatchAuth result. */
|
|
4
|
+
export class TokenResolver {
|
|
5
|
+
#client;
|
|
6
|
+
|
|
7
|
+
/** @param {object} client - ghauth gRPC client */
|
|
8
|
+
constructor(client) {
|
|
9
|
+
if (!client) throw new Error("ghauth client is required");
|
|
10
|
+
this.#client = client;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @returns {Promise<{kind: string, token?: string, authorizeUrl?: string, error?: Error}>} */
|
|
14
|
+
async resolve(surface, surfaceUserId) {
|
|
15
|
+
try {
|
|
16
|
+
const req = new ghauth.GetTokenRequest({
|
|
17
|
+
surface,
|
|
18
|
+
surface_user_id: surfaceUserId,
|
|
19
|
+
});
|
|
20
|
+
const res = await this.#client.GetToken(req);
|
|
21
|
+
switch (res.result) {
|
|
22
|
+
case "token":
|
|
23
|
+
return { kind: "token", token: res.token };
|
|
24
|
+
case "link_required":
|
|
25
|
+
return {
|
|
26
|
+
kind: "link_required",
|
|
27
|
+
authorizeUrl: res.link_required.authorize_url,
|
|
28
|
+
};
|
|
29
|
+
case "re_auth_required":
|
|
30
|
+
return { kind: "reauth_required" };
|
|
31
|
+
default:
|
|
32
|
+
return {
|
|
33
|
+
kind: "transient",
|
|
34
|
+
error: new Error("unexpected GetToken result"),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return { kind: "transient", error: err };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|