@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 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.1.0-dev.99",
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.1.0-dev.99",
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 {
@@ -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: { glasslyUserId: string; tenantId: string };
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(): { glasslyUserId: string; tenantId: string } {
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 { glasslyUserId: claims.sub, tenantId: claims.tenant_id };
287
+ return { userId: claims.sub, tenantId: claims.tenant_id };
288
288
  }
289
289
 
290
290
  /**
@@ -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 `glasslyUserId`, `tenant_id`, and `exp` for the refresh
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 `glasslyUserId`, `tenant_id` is the issuing OEM, and `exp` 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.
@@ -52,52 +52,55 @@ export type {
52
52
  } from "./support-profile";
53
53
 
54
54
  /**
55
- * A single miniapp entry as returned by the listing.
55
+ * One catalog entry from the Glassly Miniapp Store (`GET /api/client/store`).
56
56
  *
57
- * These shapes are owned by miniapp-service, which is not finalized yet, so they
58
- * are defined here against the spec rather than imported. They are intentionally
59
- * NOT taken from `@glassly/cloud-protocol`: that package is the live
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 MiniappListing {
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
- /** The version offered to this user (the latest the user is entitled to). */
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
- /** Optional one-line summary for a launcher list. */
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
- * The manifest that ships inside a miniapp bundle.
80
- *
81
- * Same ownership note as `MiniappListing`: this is a miniapp-service shape,
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
- /** What `getBundle` resolves to: where to download the bundle, plus its manifest. */
95
- export interface MiniappBundle {
96
- /** A (typically signed, short-lived) URL the device downloads the bundle from. */
97
- downloadUrl: string;
98
- /** The concrete version resolved, important when the caller omitted `version`. */
99
- version: string;
100
- manifest: MiniappManifest;
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 (only `miniapps` so far).
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.miniapps.list()`, matching the public `CoreModule` contract in the
136
- * spec. Methods are bound in the constructor so destructuring `miniapps` keeps
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
+ }