@glassly/cloud-client 0.1.0-dev.99 → 0.2.8-dev.444
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/README.md +30 -0
- package/package.json +2 -2
- package/src/http.ts +6 -0
- package/src/index.ts +5 -0
- package/src/modules/auth/auth.ts +3 -3
- package/src/modules/auth/jwt.ts +2 -2
- package/src/modules/core/core.ts +71 -70
package/README.md
CHANGED
|
@@ -29,6 +29,36 @@ npm install @glassly/cloud-client@dev
|
|
|
29
29
|
The package ships TypeScript source and targets consumers that compile TS
|
|
30
30
|
themselves (Metro / bundlers / tsc).
|
|
31
31
|
|
|
32
|
+
## Miniapp store catalog
|
|
33
|
+
|
|
34
|
+
Host integrations can browse published miniapps through an authenticated
|
|
35
|
+
`CloudClient` instance's `core.store`. This is a Core REST surface; it does
|
|
36
|
+
not require a live runtime session.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// `client` is your configured, authenticated CloudClient.
|
|
40
|
+
const {generatedAt, listings} = await client.core.store.list();
|
|
41
|
+
const listing = await client.core.store.get("com.glassly.dashboard");
|
|
42
|
+
if (listing.iconUrl) {
|
|
43
|
+
const iconBytes = await client.core.store.fetchAsset(listing.iconUrl);
|
|
44
|
+
// Cache these Uint8Array bytes locally for your UI.
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Method | Returns |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `core.store.list()` | `Promise<StoreCatalogResponse>`: `{generatedAt, listings}`; the complete catalog, for local search and tag filtering. |
|
|
51
|
+
| `core.store.get(packageName)` | `Promise<StoreListing>`: one published listing. |
|
|
52
|
+
| `core.store.fetchAsset(url)` | `Promise<Uint8Array>`: authenticated icon/screenshot bytes, accepting a listing URL or a path. |
|
|
53
|
+
|
|
54
|
+
Listings include identity, tagline, description, tags, author, featured status,
|
|
55
|
+
published version, miniapp type and persistence default, permissions, hardware
|
|
56
|
+
requirements, icon and screenshot URLs, and bundle `{url, sha256, sizeBytes}`.
|
|
57
|
+
Fetch icons and screenshots through `fetchAsset` so Core receives the user's
|
|
58
|
+
Bearer token. The host owns installation, compatibility checks, and updates;
|
|
59
|
+
listing or fetching an asset does not install a miniapp. These APIs belong to
|
|
60
|
+
the host's cloud client, not the miniapp's read-only `session.cloud` module.
|
|
61
|
+
|
|
32
62
|
## Part of Glassly
|
|
33
63
|
|
|
34
64
|
Source lives in the [Glassly monorepo](https://github.com/tetramo-labs/glassly)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/cloud-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.8-dev.444",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"test": "bun test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@glassly/cloud-protocol": "0.
|
|
15
|
+
"@glassly/cloud-protocol": "0.2.8-dev.444",
|
|
16
16
|
"tweetnacl": "^1.0.3"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
package/src/http.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface ReqOpts {
|
|
|
43
43
|
/** The REST surface the modules consume. */
|
|
44
44
|
export interface HttpClient {
|
|
45
45
|
get<T>(path: string, opts?: ReqOpts): Promise<T>;
|
|
46
|
+
/** GET a binary resource (an image or bundle) as raw bytes. */
|
|
47
|
+
getBytes(path: string, opts?: ReqOpts): Promise<Uint8Array>;
|
|
46
48
|
head(path: string, opts?: ReqOpts): Promise<Response>;
|
|
47
49
|
post<T>(path: string, body?: unknown, opts?: ReqOpts): Promise<T>;
|
|
48
50
|
postForm<T>(path: string, form: FormData, opts?: ReqOpts): Promise<T>;
|
|
@@ -243,6 +245,10 @@ export function createHttpClient(deps: CreateHttpClientDeps): HttpClient {
|
|
|
243
245
|
get<T>(path: string, opts?: ReqOpts): Promise<T> {
|
|
244
246
|
return request<T>("GET", path, undefined, opts);
|
|
245
247
|
},
|
|
248
|
+
async getBytes(path: string, opts?: ReqOpts): Promise<Uint8Array> {
|
|
249
|
+
const res = await requestRaw("GET", path, undefined, opts);
|
|
250
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
251
|
+
},
|
|
246
252
|
head(path: string, opts?: ReqOpts): Promise<Response> {
|
|
247
253
|
return requestRaw("HEAD", path, undefined, opts);
|
|
248
254
|
},
|
package/src/index.ts
CHANGED
|
@@ -46,6 +46,11 @@ export type {
|
|
|
46
46
|
PreinstalledInstallPolicy,
|
|
47
47
|
PreinstalledMiniappRegistry,
|
|
48
48
|
PreinstalledMiniappRegistryEntry,
|
|
49
|
+
StoreCatalogResponse,
|
|
50
|
+
StoreListing,
|
|
51
|
+
StoreListingHardwareRequirement,
|
|
52
|
+
StoreListingPermission,
|
|
53
|
+
StoreListingScreenshot,
|
|
49
54
|
} from "./modules/core/core";
|
|
50
55
|
|
|
51
56
|
export type {
|
package/src/modules/auth/auth.ts
CHANGED
|
@@ -65,7 +65,7 @@ export interface AuthModule {
|
|
|
65
65
|
): Promise<{ token: string; expiresAt: number }>;
|
|
66
66
|
// Core-owned user/oem identity, read from the Core access token.
|
|
67
67
|
// Runtime-only deployments do not expose this surface.
|
|
68
|
-
readonly identity: {
|
|
68
|
+
readonly identity: { userId: string; tenantId: string };
|
|
69
69
|
// refresh failed; the host must send the user back through login
|
|
70
70
|
onExpired(handler: () => void): () => void;
|
|
71
71
|
}
|
|
@@ -277,14 +277,14 @@ export class Auth implements AuthModule {
|
|
|
277
277
|
* every call, so the client need not). Throws if no Core access token has been
|
|
278
278
|
* obtained yet, since Core identity is meaningless before the first exchange.
|
|
279
279
|
*/
|
|
280
|
-
get identity(): {
|
|
280
|
+
get identity(): { userId: string; tenantId: string } {
|
|
281
281
|
this.requireCoreConfig();
|
|
282
282
|
const current = this.store.current();
|
|
283
283
|
if (!current) {
|
|
284
284
|
throw new AuthExpiredError("Core identity is unavailable before first Core sign-in");
|
|
285
285
|
}
|
|
286
286
|
const claims = decodeClaims(current.accessToken);
|
|
287
|
-
return {
|
|
287
|
+
return { userId: claims.sub, tenantId: claims.tenant_id };
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
/**
|
package/src/modules/auth/jwt.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The client is not a security boundary for its own token: the cloud verifies
|
|
5
5
|
* the signature on every call. So here we only need to read claims the client
|
|
6
|
-
* already trusts (its own `
|
|
6
|
+
* already trusts (its own `userId`, `tenant_id`, and `exp` for the refresh
|
|
7
7
|
* timing), and we deliberately skip signature checks. Doing a real verification
|
|
8
8
|
* would mean shipping the cloud's public keys to the device, which buys nothing.
|
|
9
9
|
*
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* The claims this client reads off an access token.
|
|
16
16
|
*
|
|
17
|
-
* `sub` is the `
|
|
17
|
+
* `sub` is the `userId`, `tenant_id` is the issuing OEM, and `exp` is the
|
|
18
18
|
* Unix-seconds expiry used to decide when to refresh. The index signature keeps
|
|
19
19
|
* the rest of the claims (`sessionId`, `jti`, `aud`, `iss`) accessible without
|
|
20
20
|
* naming each one, since the client only acts on these three.
|
package/src/modules/core/core.ts
CHANGED
|
@@ -52,52 +52,55 @@ export type {
|
|
|
52
52
|
} from "./support-profile";
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
55
|
+
* One catalog entry from the Glassly Miniapp Store (`GET /api/client/store`).
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* session wire contract (subscriptions, transcripts, the message unions), while
|
|
61
|
-
* a miniapp listing is a core REST resource with no place on the runtime wire.
|
|
62
|
-
* When miniapp-service locks its schema, move these to the shared package and
|
|
63
|
-
* import them here instead.
|
|
57
|
+
* Mirrors `StoreListing` in Cloud Core's store.service. Defined here rather
|
|
58
|
+
* than in `@glassly/cloud-protocol` because the catalog is a core REST
|
|
59
|
+
* resource, not part of the live session wire contract.
|
|
64
60
|
*/
|
|
65
|
-
export interface
|
|
66
|
-
/** The reverse-DNS identifier, for example "com.example.notes". */
|
|
61
|
+
export interface StoreListing {
|
|
67
62
|
packageName: string;
|
|
68
|
-
/** Human-readable name shown to the user. */
|
|
69
63
|
name: string;
|
|
70
|
-
|
|
64
|
+
tagline: string;
|
|
65
|
+
description: string;
|
|
66
|
+
tags: string[];
|
|
67
|
+
featured: boolean;
|
|
68
|
+
author: { name: string; verified: boolean };
|
|
69
|
+
/** The active (published) release version. */
|
|
71
70
|
version: string;
|
|
72
|
-
|
|
71
|
+
type: "standard" | "background" | "system_dashboard";
|
|
72
|
+
persistent: boolean;
|
|
73
|
+
permissions: StoreListingPermission[];
|
|
74
|
+
hardwareRequirements: StoreListingHardwareRequirement[];
|
|
75
|
+
/** Authenticated asset URL; fetch through `core.store.fetchAsset`. */
|
|
76
|
+
iconUrl: string | null;
|
|
77
|
+
screenshots: StoreListingScreenshot[];
|
|
78
|
+
bundle: { url: string; sha256: string; sizeBytes: number };
|
|
79
|
+
publishedAt: string;
|
|
80
|
+
updatedAt: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface StoreListingPermission {
|
|
84
|
+
type: string;
|
|
85
|
+
required?: boolean;
|
|
73
86
|
description?: string;
|
|
74
|
-
/** Optional icon URL for a launcher list. */
|
|
75
|
-
iconUrl?: string;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* defined here against the spec until that schema is locked. The fields below
|
|
83
|
-
* are the minimum the device needs to identify and describe a fetched bundle;
|
|
84
|
-
* miniapp-service is expected to add permission and capability declarations.
|
|
85
|
-
*/
|
|
86
|
-
export interface MiniappManifest {
|
|
87
|
-
packageName: string;
|
|
88
|
-
name: string;
|
|
89
|
-
version: string;
|
|
90
|
-
/** Optional permissions the miniapp declares it needs. */
|
|
91
|
-
permissions?: string[];
|
|
89
|
+
export interface StoreListingHardwareRequirement {
|
|
90
|
+
type: string;
|
|
91
|
+
level: string;
|
|
92
|
+
description?: string;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
export interface StoreListingScreenshot {
|
|
96
|
+
url: string;
|
|
97
|
+
width: number | null;
|
|
98
|
+
height: number | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface StoreCatalogResponse {
|
|
102
|
+
generatedAt: string;
|
|
103
|
+
listings: StoreListing[];
|
|
101
104
|
}
|
|
102
105
|
|
|
103
106
|
export type PreinstalledInstallPolicy =
|
|
@@ -129,19 +132,21 @@ export interface CoreDeps {
|
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
/**
|
|
132
|
-
* `cloud.core`. Stateless REST grouped by resource
|
|
135
|
+
* `cloud.core`. Stateless REST grouped by resource.
|
|
133
136
|
*
|
|
134
137
|
* The grouping is exposed as a plain object so callers write
|
|
135
|
-
* `cloud.core.
|
|
136
|
-
* spec. Methods are bound in the constructor so destructuring
|
|
137
|
-
* working.
|
|
138
|
+
* `cloud.core.store.list()`, matching the public `CoreModule` contract in the
|
|
139
|
+
* spec. Methods are bound in the constructor so destructuring keeps working.
|
|
138
140
|
*/
|
|
139
141
|
export class Core {
|
|
140
142
|
readonly miniapps: {
|
|
141
|
-
list(): Promise<MiniappListing[]>;
|
|
142
|
-
getBundle(packageName: string, version?: string): Promise<MiniappBundle>;
|
|
143
143
|
getRegistry(opts?: { environment?: string }): Promise<PreinstalledMiniappRegistry>;
|
|
144
144
|
};
|
|
145
|
+
readonly store: {
|
|
146
|
+
list(): Promise<StoreCatalogResponse>;
|
|
147
|
+
get(packageName: string): Promise<StoreListing>;
|
|
148
|
+
fetchAsset(url: string): Promise<Uint8Array>;
|
|
149
|
+
};
|
|
145
150
|
readonly reports: {
|
|
146
151
|
submit(input: SubmitReportInput): Promise<SubmitReportResult>;
|
|
147
152
|
addLogs(
|
|
@@ -165,34 +170,6 @@ export class Core {
|
|
|
165
170
|
const supportProfiles = new SupportProfiles(http);
|
|
166
171
|
|
|
167
172
|
this.miniapps = {
|
|
168
|
-
/**
|
|
169
|
-
* List the miniapps available to the authenticated user.
|
|
170
|
-
*
|
|
171
|
-
* A GET so it is naturally safe to retry on a transient network error,
|
|
172
|
-
* which the shared HTTP helper does for us.
|
|
173
|
-
*/
|
|
174
|
-
list(): Promise<MiniappListing[]> {
|
|
175
|
-
return http.get<MiniappListing[]>("/api/client/miniapps");
|
|
176
|
-
},
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Fetch the downloadable bundle for one miniapp.
|
|
180
|
-
*
|
|
181
|
-
* When `version` is omitted the cloud resolves the version the user is
|
|
182
|
-
* entitled to (usually the latest); the resolved value comes back in the
|
|
183
|
-
* response so the caller can record exactly what it got. The version is
|
|
184
|
-
* passed as a query parameter and URL-encoded so an unusual version string
|
|
185
|
-
* cannot break the path.
|
|
186
|
-
*/
|
|
187
|
-
getBundle(packageName: string, version?: string): Promise<MiniappBundle> {
|
|
188
|
-
const base = `/api/client/miniapps/${encodeURIComponent(packageName)}/bundle`;
|
|
189
|
-
const path =
|
|
190
|
-
version === undefined
|
|
191
|
-
? base
|
|
192
|
-
: `${base}?version=${encodeURIComponent(version)}`;
|
|
193
|
-
return http.get<MiniappBundle>(path);
|
|
194
|
-
},
|
|
195
|
-
|
|
196
173
|
/**
|
|
197
174
|
* Fetch the admin-managed preinstalled miniapp registry for this device.
|
|
198
175
|
*
|
|
@@ -206,6 +183,23 @@ export class Core {
|
|
|
206
183
|
return http.get<PreinstalledMiniappRegistry>(`/api/client/miniapps/registry${query}`);
|
|
207
184
|
},
|
|
208
185
|
};
|
|
186
|
+
this.store = {
|
|
187
|
+
/** The whole catalog. Small enough that search and tag filters stay on the device. */
|
|
188
|
+
list(): Promise<StoreCatalogResponse> {
|
|
189
|
+
return http.get<StoreCatalogResponse>("/api/client/store");
|
|
190
|
+
},
|
|
191
|
+
get(packageName: string): Promise<StoreListing> {
|
|
192
|
+
return http.get<StoreListing>(`/api/client/store/${encodeURIComponent(packageName)}`);
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Icons and screenshots need the user Bearer, which an <Image> cannot
|
|
196
|
+
* attach. Fetch the bytes here; the host writes them to its file cache.
|
|
197
|
+
* Accepts the absolute URL from a listing or a bare path.
|
|
198
|
+
*/
|
|
199
|
+
fetchAsset(url: string): Promise<Uint8Array> {
|
|
200
|
+
return http.getBytes(toPath(url));
|
|
201
|
+
},
|
|
202
|
+
};
|
|
209
203
|
this.reports = {
|
|
210
204
|
submit: reports.submit.bind(reports),
|
|
211
205
|
addLogs: reports.addLogs.bind(reports),
|
|
@@ -217,3 +211,10 @@ export class Core {
|
|
|
217
211
|
};
|
|
218
212
|
}
|
|
219
213
|
}
|
|
214
|
+
|
|
215
|
+
/** Listings carry absolute asset URLs; the HTTP helper joins paths onto its own base. */
|
|
216
|
+
function toPath(url: string): string {
|
|
217
|
+
if (!/^https?:\/\//i.test(url)) return url;
|
|
218
|
+
const parsed = new URL(url);
|
|
219
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
220
|
+
}
|