@basictech/react 0.7.0 → 0.8.0-beta.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/src/sync/index.ts CHANGED
@@ -4,7 +4,8 @@ import { v7 as uuidv7 } from 'uuid';
4
4
  import { Dexie } from 'dexie';
5
5
 
6
6
  import { log } from '../config'
7
- import { validateSchema, validateData } from '@basictech/schema'
7
+ import { validateData } from '@basictech/schema'
8
+ import { setTokenGetter } from './tokenRegistry'
8
9
 
9
10
  // Track initialization state
10
11
  let dexieExtensionsLoaded = false;
@@ -58,40 +59,29 @@ export class BasicSync extends Dexie {
58
59
  constructor(name: string, options: any) {
59
60
  super(name, options);
60
61
 
61
- // --- INIT SCHEMA --- //
62
-
63
- //todo: handle versions?
64
-
65
- // TODO: validate schema
62
+ // --- INIT SCHEMA --- //
66
63
  this.basic_schema = options.schema
67
64
  this.version(1).stores(this._convertSchemaToDxSchema(this.basic_schema))
68
-
69
65
  this.version(2).stores({})
70
- // this.verssion
71
66
 
72
-
73
- // create an alias for toArray
74
- // @ts-ignore
67
+ // @ts-ignore - alias for toArray
75
68
  this.Collection.prototype.get = this.Collection.prototype.toArray
76
-
77
-
78
- // --- SYNC --- //
79
-
80
- // this.syncable.on("statusChanged", (status, url) => {
81
- // console.log("statusChanged", status, url)
82
- // })
83
-
84
69
  }
85
70
 
86
- async connect({ access_token, ws_url }: { access_token: string, ws_url?: string }) {
71
+ async connect({ getToken, ws_url }: { getToken: (opts?: { forceRefresh?: boolean }) => Promise<string>, ws_url?: string }) {
87
72
  const WS_URL = ws_url || 'wss://pds.basic.id/ws'
88
73
 
89
74
  log('Connecting to', WS_URL)
90
75
 
76
+ // Store getToken in module-level registry (not in options) because
77
+ // dexie-syncable serializes options into IndexedDB via structured clone,
78
+ // which cannot handle functions.
79
+ setTokenGetter(WS_URL, getToken)
80
+
91
81
  await this.updateSyncNodes();
92
82
 
93
83
  log('Starting connection...')
94
- return this.syncable.connect("websocket", WS_URL, { authToken: access_token, schema: this.basic_schema });
84
+ return this.syncable.connect("websocket", WS_URL, { schema: this.basic_schema });
95
85
  }
96
86
 
97
87
  async disconnect({ ws_url }: { ws_url?: string } = {}) {
@@ -128,7 +118,6 @@ export class BasicSync extends Dexie {
128
118
  log(`HEISENBUG: Setting ${node.id} to ${node.id === largestNodeId ? 'master' : '0'}`);
129
119
  }
130
120
 
131
- // add delay to ensure sync nodes are updated // i dont think this helps?
132
121
  await new Promise(resolve => setTimeout(resolve, 1000));
133
122
 
134
123
  if (typeof window !== 'undefined') {
@@ -149,8 +138,10 @@ export class BasicSync extends Dexie {
149
138
 
150
139
  _convertSchemaToDxSchema(schema: any) {
151
140
  const stores = Object.entries(schema.tables).map(([key, table]: any) => {
152
-
153
- const indexedFields = Object.entries(table.fields).filter(([key, field]: any) => field.indexed).map(([key, field]: any) => `,${key}`).join('')
141
+ const indexedFields = Object.entries(table.fields)
142
+ .filter(([, field]: any) => field.indexed)
143
+ .map(([fieldKey]: any) => `,${fieldKey}`)
144
+ .join('')
154
145
  return {
155
146
  [key]: 'id' + indexedFields
156
147
  }
@@ -160,11 +151,6 @@ export class BasicSync extends Dexie {
160
151
  }
161
152
 
162
153
  debugeroo() {
163
- // console.log("debugeroo", this.syncable)
164
-
165
- // this.syncable.list().then(x => console.log(x))
166
-
167
- // this.syncable
168
154
  return this.syncable
169
155
  }
170
156
 
@@ -1,11 +1,24 @@
1
1
  "use client"
2
2
  import { Dexie } from "dexie";
3
3
  import { log } from "../config";
4
+ import { getTokenGetter } from "./tokenRegistry";
5
+
6
+ function decodeJwtExp(token) {
7
+ try {
8
+ var parts = token.split(".");
9
+ if (parts.length !== 3) return null;
10
+ var payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
11
+ return typeof payload.exp === "number" ? payload.exp : null;
12
+ } catch (_) {
13
+ return null;
14
+ }
15
+ }
4
16
 
5
17
  export const syncProtocol = function () {
6
18
  log("Initializing syncProtocol");
7
19
  // Constants:
8
20
  var RECONNECT_DELAY = 5000; // Reconnect delay in case of errors such as network down.
21
+ var TOKEN_REFRESH_BUFFER = 60; // Refresh token this many seconds before exp
9
22
 
10
23
  Dexie.Syncable.registerSyncProtocol("websocket", {
11
24
  sync: function (
@@ -24,13 +37,12 @@ export const syncProtocol = function () {
24
37
  // The following vars are needed because we must know which callback to ack when server sends it's ack to us.
25
38
  var requestId = 0;
26
39
  var acceptCallbacks = {};
40
+ var refreshTimer = null;
27
41
 
28
42
  // Connect the WebSocket to given url:
29
43
  log("Connecting to", url)
30
44
  var ws = new WebSocket(url);
31
45
 
32
- // console.log("ws OPTIONS", options);
33
-
34
46
  // sendChanges() method:
35
47
  function sendChanges(changes, baseRevision, partial, onChangesAccepted) {
36
48
  log("sendChanges", changes.length, baseRevision);
@@ -62,26 +74,72 @@ export const syncProtocol = function () {
62
74
 
63
75
 
64
76
 
65
- // When WebSocket opens, send our changes to the server.
66
- ws.onopen = function (event) {
67
- // Initiate this socket connection by sending our clientIdentity. If we dont have a clientIdentity yet,
68
- // server will call back with a new client identity that we should use in future WebSocket connections.
69
-
70
- // send the schema to the server
71
- log("Opening socket - sending clientIdentity", context.clientIdentity);
72
- ws.send(
73
- JSON.stringify({
74
- type: "clientIdentity",
75
- clientIdentity: context.clientIdentity || null,
76
- authToken: options.authToken,
77
- schema: options.schema
78
- }),
79
- );
77
+ function clearRefreshTimer() {
78
+ if (refreshTimer) {
79
+ clearTimeout(refreshTimer);
80
+ refreshTimer = null;
81
+ }
82
+ }
83
+
84
+ // Resolve the getToken function from the module-level registry.
85
+ // It's stored there (not in options) because dexie-syncable serializes
86
+ // options into IndexedDB, and functions can't survive structured clone.
87
+ function resolveGetToken() {
88
+ var fn = getTokenGetter(url);
89
+ if (!fn) throw new Error("No token getter registered for " + url);
90
+ return fn;
91
+ }
80
92
 
93
+ // Schedule a proactive token refresh before the JWT expires.
94
+ // Sends a tokenUpdate message on the existing WebSocket so the
95
+ // server can accept the new token without dropping the connection.
96
+ function scheduleTokenRefresh(tokenStr) {
97
+ clearRefreshTimer();
98
+ var exp = decodeJwtExp(tokenStr);
99
+ if (!exp) return;
100
+ var msUntilRefresh = (exp - TOKEN_REFRESH_BUFFER) * 1000 - Date.now();
101
+ if (msUntilRefresh <= 0) return;
102
+ log("Scheduling proactive token refresh in", Math.round(msUntilRefresh / 1000), "s");
103
+ refreshTimer = setTimeout(async function () {
104
+ try {
105
+ var newToken = await resolveGetToken()({ forceRefresh: true });
106
+ if (ws.readyState === WebSocket.OPEN) {
107
+ log("Sending tokenUpdate on existing WebSocket");
108
+ ws.send(JSON.stringify({ type: "tokenUpdate", authToken: newToken }));
109
+ scheduleTokenRefresh(newToken);
110
+ }
111
+ } catch (err) {
112
+ log("Proactive token refresh failed (non-fatal):", err);
113
+ }
114
+ }, msUntilRefresh);
115
+ }
116
+
117
+ // When WebSocket opens, get a fresh token and send our identity to the server.
118
+ // This runs on every open, including reconnects after ERROR_WILL_RETRY,
119
+ // so each attempt gets a fresh token via getToken().
120
+ ws.onopen = async function (event) {
121
+ try {
122
+ var token = await resolveGetToken()();
123
+ log("Opening socket - sending clientIdentity", context.clientIdentity);
124
+ ws.send(
125
+ JSON.stringify({
126
+ type: "clientIdentity",
127
+ clientIdentity: context.clientIdentity || null,
128
+ authToken: token,
129
+ schema: options.schema
130
+ }),
131
+ );
132
+ scheduleTokenRefresh(token);
133
+ } catch (err) {
134
+ log("Failed to get token for WebSocket:", err);
135
+ ws.close();
136
+ onError("Authentication failed: " + (err.message || err), RECONNECT_DELAY);
137
+ }
81
138
  };
82
139
 
83
140
  // If network down or other error, tell the framework to reconnect again in some time:
84
141
  ws.onerror = function (event) {
142
+ clearRefreshTimer();
85
143
  ws.close();
86
144
  log("ws.onerror", event);
87
145
  onError(event?.message, RECONNECT_DELAY);
@@ -89,7 +147,7 @@ export const syncProtocol = function () {
89
147
 
90
148
  // If socket is closed (network disconnected), inform framework and make it reconnect
91
149
  ws.onclose = function (event) {
92
- // console.log('🙅 ws.onclose', event)
150
+ clearRefreshTimer();
93
151
  onError("Socket closed: " + event.reason, RECONNECT_DELAY);
94
152
  };
95
153
 
@@ -114,7 +172,7 @@ export const syncProtocol = function () {
114
172
  // partial: true if server has additionalChanges to send. False if these changes were the last known. (applicable if type="changes")
115
173
  // }
116
174
  var requestFromServer = JSON.parse(event.data);
117
- log("requestFromServer", requestFromServer, { acceptCallback, isFirstRound });
175
+ log("requestFromServer", requestFromServer, { isFirstRound });
118
176
 
119
177
  if (requestFromServer.type == "clientIdentity") {
120
178
  context.clientIdentity = requestFromServer.clientIdentity;
@@ -151,8 +209,8 @@ export const syncProtocol = function () {
151
209
  onChangesAccepted,
152
210
  );
153
211
  },
154
- // Specify a disconnect function that will close our socket so that we dont continue to monitor changes.
155
212
  disconnect: function () {
213
+ clearRefreshTimer();
156
214
  ws.close();
157
215
  },
158
216
  });
@@ -164,9 +222,13 @@ export const syncProtocol = function () {
164
222
  acceptCallback(); // Tell framework that server has acknowledged the changes sent.
165
223
  delete acceptCallbacks[requestId.toString()];
166
224
  } else if (requestFromServer.type == "error") {
167
- var requestId = requestFromServer.requestId;
168
225
  ws.close();
169
- onError(requestFromServer.message, Infinity); // Don't reconnect - an error in application level means we have done something wrong.
226
+ if (requestFromServer.code === "TOKEN_EXPIRED" || requestFromServer.code === "UNAUTHORIZED") {
227
+ log("Auth error from server, will reconnect with fresh token:", requestFromServer.message);
228
+ onError(requestFromServer.message, RECONNECT_DELAY);
229
+ } else {
230
+ onError(requestFromServer.message, Infinity);
231
+ }
170
232
  } else {
171
233
  log("unknown message", requestFromServer);
172
234
  ws.close();
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Module-level registry for token getter functions, keyed by WebSocket URL.
3
+ *
4
+ * dexie-syncable serializes the `options` object into IndexedDB via
5
+ * structured clone, which cannot handle functions. This registry keeps
6
+ * the getToken function out of `options` so it survives serialization
7
+ * while remaining accessible to the sync protocol on every (re)connect.
8
+ */
9
+
10
+ type GetTokenFn = (options?: { forceRefresh?: boolean }) => Promise<string>
11
+
12
+ const registry = new Map<string, GetTokenFn>()
13
+
14
+ export function setTokenGetter(url: string, fn: GetTokenFn): void {
15
+ registry.set(url, fn)
16
+ }
17
+
18
+ export function getTokenGetter(url: string): GetTokenFn | undefined {
19
+ return registry.get(url)
20
+ }
@@ -1,12 +1,12 @@
1
1
  import { BasicStorage } from '../utils/storage'
2
2
  import { Migration } from './versionUpdater'
3
-
3
+ import { log } from '../config'
4
4
 
5
5
  export const addMigrationTimestamp: Migration = {
6
- fromVersion: '0.6.0',
6
+ fromVersion: '0.6.0',
7
7
  toVersion: '0.7.0',
8
8
  async migrate(storage: BasicStorage) {
9
- console.log('Running test migration')
9
+ log('Running migration 0.6.0 → 0.7.0')
10
10
  storage.set('test_migration', 'true')
11
11
  }
12
12
  }
@@ -1,4 +1,5 @@
1
1
  import { BasicStorage } from '../utils/storage'
2
+ import { log } from '../config'
2
3
 
3
4
  export interface VersionInfo {
4
5
  version: string
@@ -53,7 +54,7 @@ export class VersionUpdater {
53
54
  // Run migrations
54
55
  for (const migration of migrationsToRun) {
55
56
  try {
56
- console.log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`)
57
+ log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`)
57
58
  await migration.migrate(this.storage)
58
59
  } catch (error) {
59
60
  console.error(`Migration failed from ${migration.fromVersion} to ${migration.toVersion}:`, error)
@@ -89,18 +90,10 @@ export class VersionUpdater {
89
90
 
90
91
  private getMigrationsToRun(fromVersion: string, toVersion: string): Migration[] {
91
92
  return this.migrations.filter(migration => {
92
- // Migration should run if we're crossing the version boundary
93
- // i.e., stored version is less than migration.toVersion AND current version is >= migration.toVersion
94
93
  const storedLessThanMigrationTo = this.compareVersions(fromVersion, migration.toVersion) < 0
95
94
  const currentGreaterThanOrEqualMigrationTo = this.compareVersions(toVersion, migration.toVersion) >= 0
96
-
97
- console.log(`Checking migration ${migration.fromVersion} → ${migration.toVersion}:`)
98
- console.log(` stored ${fromVersion} < migration.to ${migration.toVersion}: ${storedLessThanMigrationTo}`)
99
- console.log(` current ${toVersion} >= migration.to ${migration.toVersion}: ${currentGreaterThanOrEqualMigrationTo}`)
100
-
101
95
  const shouldRun = storedLessThanMigrationTo && currentGreaterThanOrEqualMigrationTo
102
- console.log(` Should run: ${shouldRun}`)
103
-
96
+ log(`Migration ${migration.fromVersion} ${migration.toVersion}: shouldRun=${shouldRun}`)
104
97
  return shouldRun
105
98
  })
106
99
  }
@@ -1,44 +1,97 @@
1
1
  // Network utilities for Basic React package
2
+ import semver from 'semver'
2
3
  import { log } from '../config'
3
- import { version as currentVersion } from '../../package.json'
4
+ import { version as pkgVersion } from '../../package.json'
4
5
 
5
6
  export function isDevelopment(debug?: boolean): boolean {
7
+ if (debug === true) return true
8
+ if (typeof process !== 'undefined' && process.env.NODE_ENV === 'development') return true
9
+ if (typeof window === 'undefined' || !window.location) return false
10
+ const host = window.location.hostname
6
11
  return (
7
- window.location.hostname === 'localhost' ||
8
- window.location.hostname === '127.0.0.1' ||
9
- window.location.hostname.includes('localhost') ||
10
- window.location.hostname.includes('127.0.0.1') ||
11
- window.location.hostname.includes('.local') ||
12
- process.env.NODE_ENV === 'development' ||
13
- debug === true
12
+ host === 'localhost' ||
13
+ host === '127.0.0.1' ||
14
+ host.includes('localhost') ||
15
+ host.includes('127.0.0.1') ||
16
+ host.includes('.local')
14
17
  )
15
18
  }
16
19
 
20
+ function normalizeVersion(v: string | null | undefined): string | null {
21
+ if (v == null) return null
22
+ const t = String(v).trim()
23
+ return t.length ? t : null
24
+ }
25
+
26
+ function versionsMatch(a: string, b: string): boolean {
27
+ const na = a.trim()
28
+ const nb = b.trim()
29
+ if (na === nb) return true
30
+ const va = semver.valid(na)
31
+ const vb = semver.valid(nb)
32
+ if (va && vb) return semver.eq(va, vb)
33
+ return false
34
+ }
35
+
36
+ /** Use npm `beta` dist-tag when the installed version is a semver prerelease whose first id is `beta`. */
37
+ function usesBetaDistTag(version: string): boolean {
38
+ const pre = semver.prerelease(version)
39
+ const id = pre?.[0]
40
+ return typeof id === 'string' && id.toLowerCase() === 'beta'
41
+ }
42
+
43
+ type NpmInstallMeta = {
44
+ 'dist-tags'?: { latest?: string; beta?: string }
45
+ }
46
+
17
47
  export async function checkForNewVersion(): Promise<{
18
48
  hasNewVersion: boolean,
19
49
  latestVersion: string | null,
20
50
  currentVersion: string | null
21
51
  }> {
22
52
  try {
23
- const isBeta = currentVersion.includes('beta')
53
+ const currentVersion = normalizeVersion(pkgVersion)
54
+ if (!currentVersion) {
55
+ return { hasNewVersion: false, latestVersion: null, currentVersion: null }
56
+ }
24
57
 
25
- const response = await fetch(`https://registry.npmjs.org/@basictech/react/${isBeta ? 'beta' : 'latest'}`);
58
+ const response = await fetch('https://registry.npmjs.org/@basictech/react', {
59
+ headers: { Accept: 'application/vnd.npm.install-v1+json' },
60
+ })
26
61
  if (!response.ok) {
27
62
  throw new Error('Failed to fetch version from npm');
28
63
  }
29
64
 
30
- const data = await response.json();
31
- const latestVersion = data.version;
65
+ const data = (await response.json()) as NpmInstallMeta
66
+ const distTags = data['dist-tags'] ?? {}
67
+ const rawRegistry =
68
+ usesBetaDistTag(currentVersion)
69
+ ? distTags.beta ?? distTags.latest
70
+ : distTags.latest
71
+ const latestVersion = normalizeVersion(rawRegistry ?? null)
72
+ if (!latestVersion) {
73
+ throw new Error('Missing dist-tags from npm registry')
74
+ }
75
+
76
+ const same = versionsMatch(currentVersion, latestVersion)
77
+
78
+ if (!same && isDevelopment()) {
79
+ log('[basic] version check mismatch:', {
80
+ currentVersion,
81
+ registryVersion: latestVersion,
82
+ channel: usesBetaDistTag(currentVersion) ? 'beta' : 'latest',
83
+ })
84
+ }
32
85
 
33
- if (latestVersion !== currentVersion) {
86
+ if (!same) {
34
87
  console.warn('[basic] New version available:', latestVersion, `\nrun "npm install @basictech/react@${latestVersion}" to update`);
35
88
  }
36
- if (isBeta) {
89
+ if (usesBetaDistTag(currentVersion)) {
37
90
  log('thank you for being on basictech/react beta :)')
38
91
  }
39
92
 
40
93
  return {
41
- hasNewVersion: currentVersion !== latestVersion,
94
+ hasNewVersion: !same,
42
95
  latestVersion,
43
96
  currentVersion
44
97
  };
@@ -0,0 +1,22 @@
1
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
2
+
3
+ /**
4
+ * Normalize a project_id / client_id to the canonical did:web format
5
+ * before sending it to the PDS.
6
+ *
7
+ * - did:web:... -> passthrough (already canonical)
8
+ * - bare UUID -> did:web:{adminHostname}:projects:{hex}
9
+ * - "self" -> passthrough
10
+ */
11
+ export function normalizeClientId(projectId: string, adminHostname: string = 'api.basic.tech'): string {
12
+ if (!projectId) return projectId
13
+ if (projectId === 'self') return projectId
14
+ if (projectId.startsWith('did:')) return projectId
15
+
16
+ if (UUID_RE.test(projectId)) {
17
+ const hex = projectId.replace(/-/g, '').toLowerCase()
18
+ return `did:web:${adminHostname}:projects:${hex}`
19
+ }
20
+
21
+ return projectId
22
+ }
@@ -0,0 +1,101 @@
1
+ export type ResolvedDid = {
2
+ did: string
3
+ handle?: string
4
+ didDocument: Record<string, unknown>
5
+ pdsUrl: string
6
+ authorization_endpoint: string
7
+ token_endpoint: string
8
+ userinfo_endpoint: string
9
+ }
10
+
11
+ /**
12
+ * Convert a did:web DID to the HTTPS URL where its DID document lives.
13
+ *
14
+ * did:web:pds.basic.id:did:abc123 -> https://pds.basic.id/did/abc123/did.json
15
+ * did:web:example.com -> https://example.com/.well-known/did.json
16
+ */
17
+ export function resolveDidWebUrl(did: string): string | null {
18
+ if (!did.startsWith('did:web:')) return null
19
+
20
+ const rest = did.slice(8) // strip 'did:web:'
21
+ if (!rest) return null
22
+ const parts = rest.split(':')
23
+
24
+ // Decode the hostname (first part, may contain %3A for port)
25
+ const hostname = parts[0]!.replace(/%3A/gi, ':')
26
+
27
+ if (parts.length === 1) {
28
+ return `https://${hostname}/.well-known/did.json`
29
+ }
30
+
31
+ const pathParts = parts.slice(1).map(p => decodeURIComponent(p))
32
+ return `https://${hostname}/${pathParts.join('/')}/did.json`
33
+ }
34
+
35
+ /**
36
+ * Given a DID document, extract PDS URL and discover OAuth endpoints.
37
+ */
38
+ async function resolveFromDocument(did: string, didDocument: Record<string, unknown>): Promise<ResolvedDid> {
39
+ const services = didDocument.service as Array<{ id: string; type: string; serviceEndpoint: string }> | undefined
40
+ const pdsService = services?.find(
41
+ (s) => s.id === '#basic_pds' || s.id === `${did}#basic_pds`
42
+ )
43
+ if (!pdsService) {
44
+ throw new Error(`DID document has no #basic_pds service entry`)
45
+ }
46
+ const pdsUrl = pdsService.serviceEndpoint.replace(/\/+$/, '')
47
+
48
+ const oauthRes = await fetch(`${pdsUrl}/auth/.well-known/openid-configuration`)
49
+ if (!oauthRes.ok) {
50
+ throw new Error(`Failed to fetch OpenID configuration from ${pdsUrl}: ${oauthRes.status}`)
51
+ }
52
+ const oauth = await oauthRes.json()
53
+
54
+ return {
55
+ did,
56
+ didDocument,
57
+ pdsUrl,
58
+ authorization_endpoint: oauth.authorization_endpoint,
59
+ token_endpoint: oauth.token_endpoint,
60
+ userinfo_endpoint: oauth.userinfo_endpoint,
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Fetch a DID document by DID, extract the PDS URL, and discover OAuth endpoints.
66
+ */
67
+ export async function resolveDid(did: string): Promise<ResolvedDid> {
68
+ const url = resolveDidWebUrl(did)
69
+ if (!url) {
70
+ throw new Error(`Unsupported DID method: ${did}`)
71
+ }
72
+
73
+ const didRes = await fetch(url)
74
+ if (!didRes.ok) {
75
+ throw new Error(`Failed to fetch DID document at ${url}: ${didRes.status}`)
76
+ }
77
+ const didDocument = await didRes.json()
78
+
79
+ return resolveFromDocument(did, didDocument)
80
+ }
81
+
82
+ /**
83
+ * Resolve a handle (e.g. "alice.basic.id") to a DID and discover PDS + OAuth endpoints.
84
+ *
85
+ * Fetches https://{handle}/.well-known/did.json per the did:web spec.
86
+ */
87
+ export async function resolveHandle(handle: string): Promise<ResolvedDid> {
88
+ const res = await fetch(`https://${handle}/.well-known/did.json`)
89
+ if (!res.ok) {
90
+ throw new Error(`Handle resolution failed for ${handle}: ${res.status}`)
91
+ }
92
+ const didDocument = await res.json()
93
+ const did = didDocument.id as string
94
+ if (!did) {
95
+ throw new Error(`Handle response has no 'id' field`)
96
+ }
97
+
98
+ const resolved = await resolveFromDocument(did, didDocument)
99
+ resolved.handle = handle
100
+ return resolved
101
+ }
@@ -30,8 +30,6 @@ export async function getSchemaStatus(schema: any): Promise<{
30
30
  }
31
31
  })
32
32
 
33
- console.log('latestSchema', latestSchema)
34
-
35
33
  if (!latestSchema.version) {
36
34
  return {
37
35
  valid: false,
@@ -105,11 +103,12 @@ export async function validateAndCheckSchema(schema: any): Promise<{
105
103
  }
106
104
  }
107
105
 
108
- let schemaStatus = { valid: false }
106
+ let schemaStatus: { valid: boolean, status?: string, latest?: any } = { valid: false }
109
107
  if (schema.version !== 0) {
110
108
  schemaStatus = await getSchemaStatus(schema)
111
109
  log('schemaStatus', schemaStatus)
112
- } else {
110
+ } else {
111
+ schemaStatus = { valid: false, status: 'unpublished' }
113
112
  log("schema not published - at version 0")
114
113
  }
115
114
 
@@ -25,7 +25,10 @@ export const STORAGE_KEYS = {
25
25
  AUTH_STATE: 'basic_auth_state',
26
26
  REDIRECT_URI: 'basic_redirect_uri',
27
27
  SERVER_URL: 'basic_server_url',
28
- DEBUG: 'basic_debug'
28
+ PDS_ENDPOINTS: 'basic_pds_endpoints',
29
+ LAST_CONNECT_REPORT: 'basic_last_connect_report',
30
+ DEBUG: 'basic_debug',
31
+ CODE_VERIFIER: 'basic_code_verifier'
29
32
  } as const
30
33
 
31
34
  export function getCookie(name: string): string {