@flow-industries/id 0.26.4 → 0.27.0
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/contracts/v1/sdk-exports.json +150 -0
- package/dist/sdk/client/github-connector.d.ts +19 -0
- package/dist/sdk/client/github-connector.js +128 -0
- package/dist/sdk/client/index.d.ts +8 -1
- package/dist/sdk/client/index.js +5 -0
- package/dist/sdk/client/note-transport.d.ts +15 -0
- package/dist/sdk/client/note-transport.js +104 -0
- package/dist/sdk/client/oauth.d.ts +9 -0
- package/dist/sdk/client/oauth.js +68 -0
- package/dist/sdk/client/passkey-prf.d.ts +8 -0
- package/dist/sdk/client/passkey-prf.js +67 -0
- package/dist/sdk/client/session.d.ts +2 -0
- package/dist/sdk/client/session.js +23 -0
- package/dist/sdk/github/operations.d.ts +909 -0
- package/dist/sdk/github/operations.js +543 -0
- package/dist/sdk/github/scopes.d.ts +31 -0
- package/dist/sdk/github/scopes.js +70 -0
- package/dist/sdk/oauth/stores.d.ts +3 -0
- package/dist/sdk/oauth/stores.js +4 -0
- package/dist/sdk/profile.css +1 -1
- package/dist/sdk/types/atproto.d.ts +2 -0
- package/dist/sdk/types/auth.d.ts +3 -0
- package/dist/sdk/types/github-connector.d.ts +88 -0
- package/dist/sdk/types/github-connector.js +0 -0
- package/dist/sdk/types/index.d.ts +5 -2
- package/dist/sdk/types/landing.d.ts +20 -0
- package/dist/sdk/types/note-transport.d.ts +140 -0
- package/dist/sdk/types/note-transport.js +0 -0
- package/dist/sdk/types/oauth.d.ts +64 -0
- package/dist/sdk/types/oauth.js +0 -0
- package/dist/sdk/types/sdk.d.ts +5 -0
- package/package.json +1 -1
|
@@ -197,6 +197,16 @@
|
|
|
197
197
|
"type": false,
|
|
198
198
|
"value": true
|
|
199
199
|
},
|
|
200
|
+
{
|
|
201
|
+
"name": "createGitHubConnector",
|
|
202
|
+
"type": false,
|
|
203
|
+
"value": true
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "createNoteTransport",
|
|
207
|
+
"type": false,
|
|
208
|
+
"value": true
|
|
209
|
+
},
|
|
200
210
|
{
|
|
201
211
|
"name": "createProfileButton",
|
|
202
212
|
"type": false,
|
|
@@ -252,6 +262,11 @@
|
|
|
252
262
|
"type": false,
|
|
253
263
|
"value": true
|
|
254
264
|
},
|
|
265
|
+
{
|
|
266
|
+
"name": "evaluatePasskeyPrf",
|
|
267
|
+
"type": false,
|
|
268
|
+
"value": true
|
|
269
|
+
},
|
|
255
270
|
{
|
|
256
271
|
"name": "fieldsCoveredBySubjects",
|
|
257
272
|
"type": false,
|
|
@@ -347,11 +362,96 @@
|
|
|
347
362
|
"type": false,
|
|
348
363
|
"value": true
|
|
349
364
|
},
|
|
365
|
+
{
|
|
366
|
+
"name": "getConnectedPasskey",
|
|
367
|
+
"type": false,
|
|
368
|
+
"value": true
|
|
369
|
+
},
|
|
350
370
|
{
|
|
351
371
|
"name": "getFlow",
|
|
352
372
|
"type": false,
|
|
353
373
|
"value": true
|
|
354
374
|
},
|
|
375
|
+
{
|
|
376
|
+
"name": "GITHUB_BASELINE_SCOPES",
|
|
377
|
+
"type": false,
|
|
378
|
+
"value": true
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"name": "GITHUB_REQUESTABLE_SCOPES",
|
|
382
|
+
"type": false,
|
|
383
|
+
"value": true
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"name": "GITHUB_SCOPE_DESCRIPTIONS",
|
|
387
|
+
"type": false,
|
|
388
|
+
"value": true
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
"name": "GitHubConnector",
|
|
392
|
+
"type": true,
|
|
393
|
+
"value": false
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
"name": "GitHubConnectorCallOptions",
|
|
397
|
+
"type": true,
|
|
398
|
+
"value": false
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"name": "GitHubConnectorError",
|
|
402
|
+
"type": true,
|
|
403
|
+
"value": true
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"name": "GitHubConnectorErrorCode",
|
|
407
|
+
"type": true,
|
|
408
|
+
"value": false
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
"name": "GitHubConnectorFailure",
|
|
412
|
+
"type": true,
|
|
413
|
+
"value": false
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
"name": "GitHubConnectorOptions",
|
|
417
|
+
"type": true,
|
|
418
|
+
"value": false
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"name": "GitHubConnectorResult",
|
|
422
|
+
"type": true,
|
|
423
|
+
"value": false
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
"name": "githubConsentUrl",
|
|
427
|
+
"type": false,
|
|
428
|
+
"value": true
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
"name": "GitHubOperationData",
|
|
432
|
+
"type": true,
|
|
433
|
+
"value": false
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
"name": "GitHubOperationId",
|
|
437
|
+
"type": true,
|
|
438
|
+
"value": false
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"name": "GitHubOperationInput",
|
|
442
|
+
"type": true,
|
|
443
|
+
"value": false
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"name": "GitHubScope",
|
|
447
|
+
"type": true,
|
|
448
|
+
"value": false
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
"name": "hasGitHubScope",
|
|
452
|
+
"type": false,
|
|
453
|
+
"value": true
|
|
454
|
+
},
|
|
355
455
|
{
|
|
356
456
|
"name": "hitLabel",
|
|
357
457
|
"type": false,
|
|
@@ -427,6 +527,46 @@
|
|
|
427
527
|
"type": false,
|
|
428
528
|
"value": true
|
|
429
529
|
},
|
|
530
|
+
{
|
|
531
|
+
"name": "NoteBlob",
|
|
532
|
+
"type": true,
|
|
533
|
+
"value": false
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"name": "NoteCollection",
|
|
537
|
+
"type": true,
|
|
538
|
+
"value": false
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
"name": "NoteOperation",
|
|
542
|
+
"type": true,
|
|
543
|
+
"value": false
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
"name": "NoteRecord",
|
|
547
|
+
"type": true,
|
|
548
|
+
"value": false
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
"name": "NoteResponses",
|
|
552
|
+
"type": true,
|
|
553
|
+
"value": false
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
"name": "NoteStoredRecord",
|
|
557
|
+
"type": true,
|
|
558
|
+
"value": false
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
"name": "NoteTransportError",
|
|
562
|
+
"type": true,
|
|
563
|
+
"value": true
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
"name": "NoteVaultHeader",
|
|
567
|
+
"type": true,
|
|
568
|
+
"value": false
|
|
569
|
+
},
|
|
430
570
|
{
|
|
431
571
|
"name": "NotificationCounts",
|
|
432
572
|
"type": true,
|
|
@@ -462,6 +602,16 @@
|
|
|
462
602
|
"type": true,
|
|
463
603
|
"value": false
|
|
464
604
|
},
|
|
605
|
+
{
|
|
606
|
+
"name": "PasskeyPrfError",
|
|
607
|
+
"type": true,
|
|
608
|
+
"value": true
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
"name": "PasskeyPrfOptions",
|
|
612
|
+
"type": true,
|
|
613
|
+
"value": false
|
|
614
|
+
},
|
|
465
615
|
{
|
|
466
616
|
"name": "ProfileButtonHandle",
|
|
467
617
|
"type": true,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type GitHubScope } from "../github/scopes";
|
|
2
|
+
import type { GitHubConnector, GitHubConnectorErrorCode, GitHubConnectorFailure, GitHubConnectorOptions } from "../types/github-connector";
|
|
3
|
+
/** A refused connector call. `consentUrl` is set when the user can fix it in
|
|
4
|
+
* Flow ID: connecting GitHub, reconnecting, or granting `requiredScopes`. */
|
|
5
|
+
export declare class GitHubConnectorError extends Error {
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly code: GitHubConnectorErrorCode;
|
|
8
|
+
readonly requiredScopes: GitHubScope[];
|
|
9
|
+
readonly consentUrl?: string;
|
|
10
|
+
readonly retryAfter?: number;
|
|
11
|
+
readonly fields: string[];
|
|
12
|
+
constructor(status: number, failure: GitHubConnectorFailure, options?: ErrorOptions);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Calls GitHub through Flow ID as the signed-in user. Flow ID holds the
|
|
16
|
+
* user's GitHub grant and answers with projected data, so the app never sees
|
|
17
|
+
* a GitHub token. The app's origin must be on Flow ID's connector allow-list.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createGitHubConnector(host: string, getToken: () => Promise<string | null>, getUserId: () => string | null, options?: GitHubConnectorOptions): GitHubConnector;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { githubConsentUrl, isGitHubScope, } from "../github/scopes";
|
|
3
|
+
/** A refused connector call. `consentUrl` is set when the user can fix it in
|
|
4
|
+
* Flow ID: connecting GitHub, reconnecting, or granting `requiredScopes`. */
|
|
5
|
+
export class GitHubConnectorError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
code;
|
|
8
|
+
requiredScopes;
|
|
9
|
+
consentUrl;
|
|
10
|
+
retryAfter;
|
|
11
|
+
fields;
|
|
12
|
+
constructor(status, failure, options) {
|
|
13
|
+
super(failure.code, options);
|
|
14
|
+
this.status = status;
|
|
15
|
+
this.name = "GitHubConnectorError";
|
|
16
|
+
this.code = failure.code;
|
|
17
|
+
this.requiredScopes = failure.requiredScopes ?? [];
|
|
18
|
+
this.consentUrl = failure.consentUrl;
|
|
19
|
+
this.retryAfter = failure.retryAfter;
|
|
20
|
+
this.fields = failure.fields ?? [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const failureSchema = z.object({
|
|
24
|
+
code: z.string().max(64),
|
|
25
|
+
requiredScopes: z.array(z.string().max(64)).max(16).optional(),
|
|
26
|
+
consentUrl: z.string().max(2048).optional(),
|
|
27
|
+
retryAfter: z.number().int().min(0).max(3600).optional(),
|
|
28
|
+
fields: z.array(z.string().max(256)).max(20).optional(),
|
|
29
|
+
});
|
|
30
|
+
const knownCodes = new Set([
|
|
31
|
+
"GITHUB_CONNECTOR_DISABLED",
|
|
32
|
+
"ORIGIN_DENIED",
|
|
33
|
+
"SIGN_IN_REQUIRED",
|
|
34
|
+
"ACCOUNT_CHANGED",
|
|
35
|
+
"RATE_LIMITED",
|
|
36
|
+
"UNKNOWN_OPERATION",
|
|
37
|
+
"INVALID_BODY",
|
|
38
|
+
"TOO_LARGE",
|
|
39
|
+
"GITHUB_NOT_CONNECTED",
|
|
40
|
+
"GITHUB_RECONNECT_REQUIRED",
|
|
41
|
+
"GITHUB_SCOPE_REQUIRED",
|
|
42
|
+
"GITHUB_RATE_LIMITED",
|
|
43
|
+
"GITHUB_NOT_FOUND",
|
|
44
|
+
"GITHUB_FORBIDDEN",
|
|
45
|
+
"GITHUB_CONFLICT",
|
|
46
|
+
"GITHUB_VALIDATION_FAILED",
|
|
47
|
+
"GITHUB_UNAVAILABLE",
|
|
48
|
+
"GITHUB_INVALID_RESPONSE",
|
|
49
|
+
]);
|
|
50
|
+
function isKnownCode(code) {
|
|
51
|
+
return knownCodes.has(code);
|
|
52
|
+
}
|
|
53
|
+
const successSchema = z.object({
|
|
54
|
+
data: z.unknown(),
|
|
55
|
+
nextPage: z.number().int().min(1).max(1000).nullable(),
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* Calls GitHub through Flow ID as the signed-in user. Flow ID holds the
|
|
59
|
+
* user's GitHub grant and answers with projected data, so the app never sees
|
|
60
|
+
* a GitHub token. The app's origin must be on Flow ID's connector allow-list.
|
|
61
|
+
*/
|
|
62
|
+
export function createGitHubConnector(host, getToken, getUserId, options = {}) {
|
|
63
|
+
// Never call `fetch` as a method of another object: Chrome throws
|
|
64
|
+
// "Illegal invocation" for any receiver other than the window.
|
|
65
|
+
const send = options.fetch ??
|
|
66
|
+
((input, init) => globalThis.fetch(input, init));
|
|
67
|
+
const consentUrl = (scopes = []) => githubConsentUrl(host, scopes);
|
|
68
|
+
return {
|
|
69
|
+
async call(operation, input, callOptions = {}) {
|
|
70
|
+
const userId = getUserId();
|
|
71
|
+
const token = userId ? await getToken() : null;
|
|
72
|
+
if (!userId || !token)
|
|
73
|
+
throw new GitHubConnectorError(401, { code: "SIGN_IN_REQUIRED" });
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await send(`${host}/api/connectors/github/${encodeURIComponent(operation)}`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: {
|
|
79
|
+
Authorization: `Bearer ${token}`,
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
"X-Flow-User-Id": userId,
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(input),
|
|
84
|
+
signal: callOptions.signal,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
callOptions.signal?.throwIfAborted();
|
|
89
|
+
throw new GitHubConnectorError(0, { code: "CONNECTOR_UNAVAILABLE" }, {
|
|
90
|
+
cause: error,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const payload = await response.json().catch(() => null);
|
|
94
|
+
if (getUserId() !== userId)
|
|
95
|
+
throw new GitHubConnectorError(409, { code: "ACCOUNT_CHANGED" });
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const failure = failureSchema.safeParse(payload);
|
|
98
|
+
throw new GitHubConnectorError(response.status, failure.success && isKnownCode(failure.data.code)
|
|
99
|
+
? {
|
|
100
|
+
...failure.data,
|
|
101
|
+
code: failure.data.code,
|
|
102
|
+
requiredScopes: failure.data.requiredScopes?.filter(isGitHubScope),
|
|
103
|
+
}
|
|
104
|
+
: { code: "CONNECTOR_UNAVAILABLE" });
|
|
105
|
+
}
|
|
106
|
+
const success = successSchema.safeParse(payload);
|
|
107
|
+
if (!success.success)
|
|
108
|
+
throw new GitHubConnectorError(502, {
|
|
109
|
+
code: "GITHUB_INVALID_RESPONSE",
|
|
110
|
+
});
|
|
111
|
+
/* SAFETY: Flow ID validated `data` against this operation's response schema
|
|
112
|
+
before answering; the SDK names that contract rather than re-validating it. */
|
|
113
|
+
return success.data;
|
|
114
|
+
},
|
|
115
|
+
consentUrl,
|
|
116
|
+
requestScopes(scopes = []) {
|
|
117
|
+
if (!("window" in globalThis))
|
|
118
|
+
return null;
|
|
119
|
+
// Open blank first so the opener link is cut before Flow ID loads.
|
|
120
|
+
const opened = window.open("about:blank", "_blank");
|
|
121
|
+
if (!opened)
|
|
122
|
+
return null;
|
|
123
|
+
opened.opener = null;
|
|
124
|
+
opened.location.href = consentUrl(scopes);
|
|
125
|
+
return opened;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
export { GITHUB_BASELINE_SCOPES, GITHUB_REQUESTABLE_SCOPES, GITHUB_SCOPE_DESCRIPTIONS, githubConsentUrl, hasGitHubScope, } from "../github/scopes";
|
|
1
2
|
export { defaultIdHost, isLocalHostname } from "../id-host";
|
|
2
|
-
export type { AccessKeyOptions, ActionCatalogEntry, ActionCatalogResponse, ActionCurve, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActionSurface, ActionsApi, ActionTransitionKind, ActiveAction, ActiveActionResponse, ActiveSessionConflict, AdditionalSession, Address, AtprotoApi, AtprotoResolution, AtprotoResolutionFailure, AtprotoResolveOptions, BrowserAccount, BrowserAccounts, CatalogFieldRef, ClockAnchor, ConnectCapabilities, ConnectResponse, CreateFlowOptions, CreateStudySubjectInput, CurrentAction, DialogHost, FinishActionRequest, FinishActionResponse, FinishedSummary, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, FlowWidgetHandle, FlowWidgetName, FocusApi, FocusPhase, FocusSessionEvent, FocusSessionState, ListStudySessionsOptions, ListStudySubjectsOptions, LoginOptions, MarkNotificationsReadInput, MethodName, MountFlowWidgetOptions, MountProfileOptions, NotificationCounts, NotificationEntry, NotificationPage, NotificationReadResult, NotificationRefusal, OpenProfileOptions, ProfileButtonHandle, ProfilePosition, RoomMemberEntry, Session, StartActionInput, StartActionResponse, StartActionResult, StartFocusRequest, StudyApi, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySearchHit, StudySessionEntry, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRefusal, StudySubjectRemovalResponse, StudySubjectResponse, StudySubjectTotals, StudySummary, SwitchAccountOptions, UpdateStudySubjectInput, UserActionSurface, XpEstimateContext, XpMode, } from "../types";
|
|
3
|
+
export type { AccessKeyOptions, ActionCatalogEntry, ActionCatalogResponse, ActionCurve, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActionSurface, ActionsApi, ActionTransitionKind, ActiveAction, ActiveActionResponse, ActiveSessionConflict, AdditionalSession, Address, AtprotoApi, AtprotoResolution, AtprotoResolutionFailure, AtprotoResolveOptions, BrowserAccount, BrowserAccounts, CatalogFieldRef, ClockAnchor, ConnectCapabilities, ConnectResponse, CreateFlowOptions, CreateStudySubjectInput, CurrentAction, DialogHost, FinishActionRequest, FinishActionResponse, FinishedSummary, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, FlowWidgetHandle, FlowWidgetName, FocusApi, FocusPhase, FocusSessionEvent, FocusSessionState, ListStudySessionsOptions, ListStudySubjectsOptions, LoginOptions, MarkNotificationsReadInput, MethodName, MountFlowWidgetOptions, MountProfileOptions, NotificationCounts, NotificationEntry, NotificationPage, NotificationReadResult, NotificationRefusal, OpenProfileOptions, PasskeyPrfOptions, ProfileButtonHandle, ProfilePosition, RoomMemberEntry, Session, StartActionInput, StartActionResponse, StartActionResult, StartFocusRequest, StudyApi, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySearchHit, StudySessionEntry, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRefusal, StudySubjectRemovalResponse, StudySubjectResponse, StudySubjectTotals, StudySummary, SwitchAccountOptions, UpdateStudySubjectInput, UserActionSurface, XpEstimateContext, XpMode, } from "../types";
|
|
4
|
+
export type { GitHubConnector, GitHubConnectorCallOptions, GitHubConnectorErrorCode, GitHubConnectorFailure, GitHubConnectorOptions, GitHubConnectorResult, GitHubOperationData, GitHubOperationId, GitHubOperationInput, GitHubScope, } from "../types/github-connector";
|
|
5
|
+
export type { NoteBlob, NoteCollection, NoteOperation, NoteRecord, NoteResponses, NoteStoredRecord, NoteVaultHeader, } from "../types/note-transport";
|
|
3
6
|
export { consistencyCurve, estimateActionXp, FLOW_SCORE_MAX, flowMultiplier, } from "../xp/curve";
|
|
4
7
|
export { ActionsRequestError, createActionsApi, isActiveSessionConflict, } from "./actions";
|
|
5
8
|
export { AtprotoRequestError, createAtprotoApi } from "./atproto-resolver";
|
|
@@ -7,11 +10,15 @@ export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
|
|
|
7
10
|
export { createDialogHost } from "./dialog-host";
|
|
8
11
|
export { createFlowWidget } from "./flow-widget";
|
|
9
12
|
export { actionHeadline, anchorFor, catalogFieldIndex, createFocusApi, elapsedSeconds, fieldsCoveredBySubjects, focusSessionReducer, formatElapsed, hitLabel, INITIAL_FOCUS_STATE, MEDITATION_SOURCE, matchScore, normalizeSearchText, REANCHOR_TOLERANCE_SECONDS, recentSubjects, STUDY_SOURCE, searchStudy, shouldReanchor, } from "./focus";
|
|
13
|
+
export { createGitHubConnector, GitHubConnectorError, } from "./github-connector";
|
|
10
14
|
export { METHODS } from "./methods";
|
|
15
|
+
export { createNoteTransport, NoteTransportError } from "./note-transport";
|
|
11
16
|
export { closeProfile, openProfile } from "./open-profile";
|
|
17
|
+
export { evaluatePasskeyPrf, PasskeyPrfError } from "./passkey-prf";
|
|
12
18
|
export { createProfileButton } from "./profile-button";
|
|
13
19
|
export type { ReportOperation, ReportRequest, ReportResponse } from "./reports";
|
|
14
20
|
export { createReportsApi, ReportRequestError } from "./reports";
|
|
15
21
|
export { createRoomsApi, RoomsRequestError } from "./rooms";
|
|
22
|
+
export { getConnectedPasskey } from "./session";
|
|
16
23
|
export { createStaticFlow } from "./static-flow";
|
|
17
24
|
export { createStudyApi, StudyRequestError } from "./study";
|
package/dist/sdk/client/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { GITHUB_BASELINE_SCOPES, GITHUB_REQUESTABLE_SCOPES, GITHUB_SCOPE_DESCRIPTIONS, githubConsentUrl, hasGitHubScope, } from "../github/scopes";
|
|
1
2
|
export { defaultIdHost, isLocalHostname } from "../id-host";
|
|
2
3
|
export { consistencyCurve, estimateActionXp, FLOW_SCORE_MAX, flowMultiplier, } from "../xp/curve";
|
|
3
4
|
export { ActionsRequestError, createActionsApi, isActiveSessionConflict, } from "./actions";
|
|
@@ -6,10 +7,14 @@ export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
|
|
|
6
7
|
export { createDialogHost } from "./dialog-host";
|
|
7
8
|
export { createFlowWidget } from "./flow-widget";
|
|
8
9
|
export { actionHeadline, anchorFor, catalogFieldIndex, createFocusApi, elapsedSeconds, fieldsCoveredBySubjects, focusSessionReducer, formatElapsed, hitLabel, INITIAL_FOCUS_STATE, MEDITATION_SOURCE, matchScore, normalizeSearchText, REANCHOR_TOLERANCE_SECONDS, recentSubjects, STUDY_SOURCE, searchStudy, shouldReanchor, } from "./focus";
|
|
10
|
+
export { createGitHubConnector, GitHubConnectorError, } from "./github-connector";
|
|
9
11
|
export { METHODS } from "./methods";
|
|
12
|
+
export { createNoteTransport, NoteTransportError } from "./note-transport";
|
|
10
13
|
export { closeProfile, openProfile } from "./open-profile";
|
|
14
|
+
export { evaluatePasskeyPrf, PasskeyPrfError } from "./passkey-prf";
|
|
11
15
|
export { createProfileButton } from "./profile-button";
|
|
12
16
|
export { createReportsApi, ReportRequestError } from "./reports";
|
|
13
17
|
export { createRoomsApi, RoomsRequestError } from "./rooms";
|
|
18
|
+
export { getConnectedPasskey } from "./session";
|
|
14
19
|
export { createStaticFlow } from "./static-flow";
|
|
15
20
|
export { createStudyApi, StudyRequestError } from "./study";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AtprotoApi, AtprotoResolution } from "../types/atproto";
|
|
2
|
+
import type { NoteBlob, NoteOperation, NoteResponses } from "../types/note-transport";
|
|
3
|
+
export declare class NoteTransportError extends Error {
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly status: number;
|
|
6
|
+
constructor(code: string, status: number);
|
|
7
|
+
}
|
|
8
|
+
/** Ciphertext-only Auth transport; the browser never receives provider tokens or DPoP keys. */
|
|
9
|
+
export declare function createNoteTransport(host: string, getToken: () => Promise<string | null>, getUserId: () => string | null, getSessionVersion: () => number, resolver: AtprotoApi, expectedIdentity: Pick<AtprotoResolution, "did" | "identityVersion">): {
|
|
10
|
+
records<T extends NoteOperation>(operation: T, signal?: AbortSignal): Promise<NoteResponses[T["method"]]>;
|
|
11
|
+
download(cid: string, signal?: AbortSignal): Promise<Uint8Array<ArrayBuffer>>;
|
|
12
|
+
upload(bytes: Uint8Array<ArrayBuffer>, signal?: AbortSignal): Promise<{
|
|
13
|
+
blob: NoteBlob;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export class NoteTransportError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
constructor(code, status) {
|
|
6
|
+
super(code);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.name = "NoteTransportError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Ciphertext-only Auth transport; the browser never receives provider tokens or DPoP keys. */
|
|
13
|
+
export function createNoteTransport(host, getToken, getUserId, getSessionVersion, resolver, expectedIdentity) {
|
|
14
|
+
const pinnedIdentity = { ...expectedIdentity };
|
|
15
|
+
async function request(action, body, signal) {
|
|
16
|
+
signal?.throwIfAborted();
|
|
17
|
+
const userId = getUserId();
|
|
18
|
+
const version = getSessionVersion();
|
|
19
|
+
if (!userId)
|
|
20
|
+
throw new NoteTransportError("SIGN_IN_REQUIRED", 401);
|
|
21
|
+
const unchanged = () => {
|
|
22
|
+
if (getUserId() !== userId || getSessionVersion() !== version)
|
|
23
|
+
throw new NoteTransportError("ACCOUNT_CHANGED", 409);
|
|
24
|
+
};
|
|
25
|
+
const resolution = await resolver.resolve({ freshness: "refresh", signal });
|
|
26
|
+
unchanged();
|
|
27
|
+
if (resolution.did !== pinnedIdentity.did ||
|
|
28
|
+
resolution.identityVersion !== pinnedIdentity.identityVersion) {
|
|
29
|
+
resolver.invalidate();
|
|
30
|
+
throw new NoteTransportError("ACCOUNT_CHANGED", 409);
|
|
31
|
+
}
|
|
32
|
+
if (resolution.lifecycle !== "active" ||
|
|
33
|
+
resolution.authorization !== "connected")
|
|
34
|
+
throw new NoteTransportError("RECONNECT_REQUIRED", 403);
|
|
35
|
+
const token = await getToken();
|
|
36
|
+
unchanged();
|
|
37
|
+
if (!token)
|
|
38
|
+
throw new NoteTransportError("SIGN_IN_REQUIRED", 401);
|
|
39
|
+
let response;
|
|
40
|
+
try {
|
|
41
|
+
response = await fetch(`${host}/api/atproto/note/${action}`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
Authorization: `Bearer ${token}`,
|
|
45
|
+
"X-Flow-User-Id": userId,
|
|
46
|
+
"X-Flow-At-Did": pinnedIdentity.did,
|
|
47
|
+
"X-Flow-Identity-Version": pinnedIdentity.identityVersion,
|
|
48
|
+
"Content-Type": action === "upload"
|
|
49
|
+
? "application/octet-stream"
|
|
50
|
+
: "application/json",
|
|
51
|
+
},
|
|
52
|
+
body,
|
|
53
|
+
signal,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
unchanged();
|
|
59
|
+
throw new NoteTransportError("PDS_UNAVAILABLE", 503);
|
|
60
|
+
}
|
|
61
|
+
const data = action === "download" && response.ok
|
|
62
|
+
? new Uint8Array(await response.arrayBuffer())
|
|
63
|
+
: await response.json().catch(() => null);
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
|
+
unchanged();
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const parsed = z.object({ code: z.string() }).safeParse(data);
|
|
68
|
+
const code = parsed.success ? parsed.data.code : "PDS_UNAVAILABLE";
|
|
69
|
+
if (code === "ACCOUNT_CHANGED" || code === "RECONNECT_REQUIRED")
|
|
70
|
+
resolver.invalidate();
|
|
71
|
+
throw new NoteTransportError(code, response.status);
|
|
72
|
+
}
|
|
73
|
+
const after = await resolver.resolve({ freshness: "refresh", signal });
|
|
74
|
+
unchanged();
|
|
75
|
+
if (after.did !== pinnedIdentity.did ||
|
|
76
|
+
after.identityVersion !== pinnedIdentity.identityVersion) {
|
|
77
|
+
resolver.invalidate();
|
|
78
|
+
throw new NoteTransportError("ACCOUNT_CHANGED", 409);
|
|
79
|
+
}
|
|
80
|
+
if (data instanceof Uint8Array)
|
|
81
|
+
return new Uint8Array(data);
|
|
82
|
+
const parsed = z.record(z.string(), z.unknown()).safeParse(data);
|
|
83
|
+
if (!parsed.success)
|
|
84
|
+
throw new NoteTransportError("INVALID_RESPONSE", 502);
|
|
85
|
+
// SAFETY: Auth validates method-specific ciphertext schemas and strips provider secrets; the SDK preserves those named contracts.
|
|
86
|
+
return parsed.data;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
async records(operation, signal) {
|
|
90
|
+
// SAFETY: Auth validates the selected operation's named response schema.
|
|
91
|
+
return (await request("records", JSON.stringify(operation), signal));
|
|
92
|
+
},
|
|
93
|
+
async download(cid, signal) {
|
|
94
|
+
const result = await request("download", JSON.stringify({ cid }), signal);
|
|
95
|
+
if (!(result instanceof Uint8Array))
|
|
96
|
+
throw new NoteTransportError("INVALID_RESPONSE", 502);
|
|
97
|
+
return result;
|
|
98
|
+
},
|
|
99
|
+
async upload(bytes, signal) {
|
|
100
|
+
// SAFETY: Auth validates the upload response as a single standard octet-stream blob reference.
|
|
101
|
+
return (await request("upload", bytes, signal));
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OAuthCompletion, OAuthStartRequest, OAuthStatus } from "../types";
|
|
2
|
+
/** The provider a hosted-page or account-page URL is returning from, if any. */
|
|
3
|
+
export declare function oauthReturnProvider(search: string): string | null;
|
|
4
|
+
export declare function getOAuthStatus(provider: string, signal?: AbortSignal): Promise<OAuthStatus>;
|
|
5
|
+
export declare function unlinkOAuth(provider: string, expectedUserId: string): Promise<void>;
|
|
6
|
+
/** Starts authorization and leaves the page for the provider; resolves only on failure paths. */
|
|
7
|
+
export declare function startOAuth(provider: string, request: OAuthStartRequest, signal: AbortSignal): Promise<void>;
|
|
8
|
+
/** Finishes the attempt this tab started; memoized so a re-render cannot spend the token twice. */
|
|
9
|
+
export declare function completeOAuth(provider: string): Promise<OAuthCompletion>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** The provider a hosted-page or account-page URL is returning from, if any. */
|
|
2
|
+
export function oauthReturnProvider(search) {
|
|
3
|
+
return new URLSearchParams(search).get("oauth");
|
|
4
|
+
}
|
|
5
|
+
const endpoint = (provider, action) => `/api/auth/oauth/${encodeURIComponent(provider)}/${action}`;
|
|
6
|
+
const completionKey = (provider) => `flow.oauth.completion.${provider}`;
|
|
7
|
+
async function readResponse(response) {
|
|
8
|
+
const body = await response.json().catch(() => ({}));
|
|
9
|
+
if (!response.ok)
|
|
10
|
+
throw new Error(body.message ?? body.error ?? "Could not connect your account.");
|
|
11
|
+
// SAFETY: the caller names the payload returned by the same-origin Flow endpoint.
|
|
12
|
+
return body;
|
|
13
|
+
}
|
|
14
|
+
export async function getOAuthStatus(provider, signal) {
|
|
15
|
+
return readResponse(await fetch(endpoint(provider, "status"), {
|
|
16
|
+
credentials: "include",
|
|
17
|
+
signal,
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
export async function unlinkOAuth(provider, expectedUserId) {
|
|
21
|
+
await readResponse(await fetch(endpoint(provider, "unlink"), {
|
|
22
|
+
method: "POST",
|
|
23
|
+
credentials: "include",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify({ expectedUserId }),
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
/** Starts authorization and leaves the page for the provider; resolves only on failure paths. */
|
|
29
|
+
export async function startOAuth(provider, request, signal) {
|
|
30
|
+
signal.throwIfAborted();
|
|
31
|
+
const result = await readResponse(await fetch(endpoint(provider, "start"), {
|
|
32
|
+
method: "POST",
|
|
33
|
+
credentials: "include",
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
body: JSON.stringify(request),
|
|
36
|
+
signal,
|
|
37
|
+
}));
|
|
38
|
+
signal.throwIfAborted();
|
|
39
|
+
sessionStorage.setItem(completionKey(provider), result.completionToken);
|
|
40
|
+
window.location.assign(result.url);
|
|
41
|
+
}
|
|
42
|
+
const completions = new Map();
|
|
43
|
+
/** Finishes the attempt this tab started; memoized so a re-render cannot spend the token twice. */
|
|
44
|
+
export function completeOAuth(provider) {
|
|
45
|
+
const existing = completions.get(provider);
|
|
46
|
+
if (existing)
|
|
47
|
+
return existing;
|
|
48
|
+
const completionToken = sessionStorage.getItem(completionKey(provider));
|
|
49
|
+
if (!completionToken)
|
|
50
|
+
return Promise.reject(new Error("Sign-in expired. Please try again."));
|
|
51
|
+
const completion = fetch(endpoint(provider, "complete"), {
|
|
52
|
+
method: "POST",
|
|
53
|
+
credentials: "include",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({ completionToken }),
|
|
56
|
+
})
|
|
57
|
+
.then(async (response) => {
|
|
58
|
+
if (response.status === 202)
|
|
59
|
+
throw new Error("Authorization is still finishing. Please try again.");
|
|
60
|
+
return readResponse(response);
|
|
61
|
+
})
|
|
62
|
+
.then((result) => {
|
|
63
|
+
sessionStorage.removeItem(completionKey(provider));
|
|
64
|
+
return result;
|
|
65
|
+
});
|
|
66
|
+
completions.set(provider, completion);
|
|
67
|
+
return completion;
|
|
68
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FlowCredential } from "../types/auth";
|
|
2
|
+
import type { PasskeyPrfOptions } from "../types/sdk";
|
|
3
|
+
export declare class PasskeyPrfError extends Error {
|
|
4
|
+
readonly code: "PASSKEY_REQUIRED" | "PRF_UNAVAILABLE" | "ACCOUNT_CHANGED" | "INVALID_INPUT";
|
|
5
|
+
constructor(code: "PASSKEY_REQUIRED" | "PRF_UNAVAILABLE" | "ACCOUNT_CHANGED" | "INVALID_INPUT");
|
|
6
|
+
}
|
|
7
|
+
/** Evaluate only the connected passkey. Output is client-only key material: never serialize or log it. */
|
|
8
|
+
export declare function evaluatePasskeyPrf(getCredential: () => FlowCredential | null, getUserId: () => string | null, getSessionVersion: () => number, options: PasskeyPrfOptions): Promise<Uint8Array<ArrayBuffer>>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export class PasskeyPrfError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code) {
|
|
4
|
+
super(code);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "PasskeyPrfError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/** Evaluate only the connected passkey. Output is client-only key material: never serialize or log it. */
|
|
10
|
+
export async function evaluatePasskeyPrf(getCredential, getUserId, getSessionVersion, options) {
|
|
11
|
+
options.signal?.throwIfAborted();
|
|
12
|
+
const credential = getCredential();
|
|
13
|
+
const userId = getUserId();
|
|
14
|
+
const sessionVersion = getSessionVersion();
|
|
15
|
+
if (!credential || !userId)
|
|
16
|
+
throw new PasskeyPrfError("PASSKEY_REQUIRED");
|
|
17
|
+
if (options.input.byteLength !== 32 || !options.rpId)
|
|
18
|
+
throw new PasskeyPrfError("INVALID_INPUT");
|
|
19
|
+
let id;
|
|
20
|
+
try {
|
|
21
|
+
if (!/^[A-Za-z0-9_-]+$/.test(credential.id))
|
|
22
|
+
throw new Error();
|
|
23
|
+
id = Uint8Array.from(atob(credential.id.replace(/-/g, "+").replace(/_/g, "/")), (character) => character.charCodeAt(0));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new PasskeyPrfError("INVALID_INPUT");
|
|
27
|
+
}
|
|
28
|
+
const unchanged = () => {
|
|
29
|
+
if (getSessionVersion() !== sessionVersion ||
|
|
30
|
+
getUserId() !== userId ||
|
|
31
|
+
getCredential()?.id !== credential.id)
|
|
32
|
+
throw new PasskeyPrfError("ACCOUNT_CHANGED");
|
|
33
|
+
};
|
|
34
|
+
if (!globalThis.navigator?.credentials || !globalThis.PublicKeyCredential)
|
|
35
|
+
throw new PasskeyPrfError("PRF_UNAVAILABLE");
|
|
36
|
+
let assertion;
|
|
37
|
+
try {
|
|
38
|
+
assertion = await navigator.credentials.get({
|
|
39
|
+
publicKey: {
|
|
40
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
41
|
+
rpId: options.rpId,
|
|
42
|
+
allowCredentials: [{ type: "public-key", id }],
|
|
43
|
+
userVerification: "required",
|
|
44
|
+
timeout: 60_000,
|
|
45
|
+
extensions: { prf: { eval: { first: options.input.slice().buffer } } },
|
|
46
|
+
},
|
|
47
|
+
signal: options.signal,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
options.signal?.throwIfAborted();
|
|
52
|
+
unchanged();
|
|
53
|
+
throw new PasskeyPrfError("PRF_UNAVAILABLE");
|
|
54
|
+
}
|
|
55
|
+
options.signal?.throwIfAborted();
|
|
56
|
+
unchanged();
|
|
57
|
+
if (!(assertion instanceof PublicKeyCredential))
|
|
58
|
+
throw new PasskeyPrfError("PRF_UNAVAILABLE");
|
|
59
|
+
const actualId = new Uint8Array(assertion.rawId);
|
|
60
|
+
if (actualId.length !== id.length ||
|
|
61
|
+
actualId.some((byte, i) => byte !== id[i]))
|
|
62
|
+
throw new PasskeyPrfError("PRF_UNAVAILABLE");
|
|
63
|
+
const result = assertion.getClientExtensionResults().prf?.results?.first;
|
|
64
|
+
if (!(result instanceof ArrayBuffer) || result.byteLength !== 32)
|
|
65
|
+
throw new PasskeyPrfError("PRF_UNAVAILABLE");
|
|
66
|
+
return new Uint8Array(result.slice(0));
|
|
67
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Address, FlowCredential, FlowState, Store } from "../types";
|
|
2
|
+
/** Reads cached public passkey metadata only; this does not establish a signed-in session or evaluate PRF. */
|
|
3
|
+
export declare function getConnectedPasskey(): Promise<FlowCredential | null>;
|
|
2
4
|
/**
|
|
3
5
|
* Reads the active credential from IndexedDB and seeds the in-memory store
|
|
4
6
|
* with `{ credential, address }`. Run on every page load so signing operations
|