@crvouga/mockingbird-service-oura 1.7.0 → 1.8.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/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Changelog — @crvouga/mockingbird-service-oura
2
2
 
3
- ## 1.7.0 (2026-10-19)
3
+ ## 1.8.0 (2026-10-21)
4
+
5
+ ### Features
6
+
7
+ - raise the serve and fleet body limit to the S3 maximum ([86b9e2b](https://github.com/crvouga/mockingbird/commit/86b9e2b80bcaaaef57f52edbac50b27da0b42f33))
8
+ - add authorize endpoints that redirect with code and state, and profile endpoints ([a686188](https://github.com/crvouga/mockingbird/commit/a6861889e02552eb31ea7f8c505db12ffc450072))
9
+
10
+ ## 1.7.0 (2026-10-18)
4
11
 
5
12
  ### Features
6
13
 
package/README.md CHANGED
@@ -32,6 +32,12 @@ Run `mockingbird-oura serve --port 12128`; inject this origin into the consumer'
32
32
 
33
33
  POST `/oauth/token` accepts URL-encoded authorization code or refresh grants, Basic or body client credentials. Seed `clients`, `codes`, `grants` with constructor options or standard state admin routes. Default local client is `mock_client` / `mock_client_secret`; bearer `mock_oura_token` belongs to `synthetic-user`. Codes bind client, redirect, expiry and optional S256 PKCE challenge. Refresh tokens rotate once, retaining user/scopes; clock controls access expiry.
34
34
 
35
+ GET `/oauth/authorize` is the authorization endpoint a browser visits to connect a member (live: `cloud.ouraring.com/oauth/authorize`). There is no consent page: the request is validated, then the `consents` collection answers for the user and the browser is redirected (302) to `redirect_uri` with `code` and `state`, or `error`, `error_description` and `state`. The code is stored in `codes` bound to the client, the granted scopes, the `redirect_uri` when the caller sent one (the token exchange must then repeat it) and an optional S256 `code_challenge` (the exchange then needs the matching `code_verifier`); `POST /oauth/token` redeems it once. `scope` omitted grants every scope; `state` is optional and echoed unchanged at any length. `redirect_uri` may be omitted when the client has exactly one registered. An unknown client (401 `invalid_client`), a missing `client_id`, a missing or unregistered `redirect_uri` (400 `invalid_request`, JSON `{status,error,error_description}`) is answered in place and never redirected to. Clients take optional `redirectUris` (exact match; omitted accepts any absolute http(s) URI so the default `mock_client` works as is) and `scopes` (omitted allows the documented Oura scopes: `email personal daily heartrate workout tag session spo2`). Redirected errors: `invalid_request` (no `response_type`, or a `code_challenge_method` other than `S256`), `unsupported_response_type`, `invalid_scope`.
36
+
37
+ Consent controls: rows `{id, decision: "approve"|"deny"|"error", userId?, codeTtlSeconds?, error?, errorDescription?}` where `id` is a client id or `*` (every client without its own row). Seed with the `consents` option or `POST /__admin/state/consents {"id":"*","value":{"id":"*","decision":"deny"}}`. No row means approve for `synthetic-user` with a 600 second code. `deny` redirects `access_denied` and `error` redirects `server_error` (override with `error`/`errorDescription`), both with the unchanged `state` and no code. Issued codes are listed at `GET /__admin/state/codes`.
38
+
39
+ GET `/v2/usercollection/personal_info` returns the token user's personal info (`id`, and as seeded `age`, `weight`, `height`, `biological_sex`, `email`): seed a `records` row with `collection: "personal_info"`, `userId` the token's user and `data` the response object (fake values only). Scope `personal` is required (403 `{detail:"Access forbidden."}` like the collections, 401 with the collections' body for an unknown or expired token, 404 `{detail:"Not Found"}` when the user has no row). `data` is returned as seeded: the `email` scope is not enforced per field. The default grant does not carry `personal`.
40
+
35
41
  GET `/v2/usercollection/{workout,sleep,heartrate,daily_activity,daily_spo2,daily_readiness,daily_sleep}` returns `{data,next_token}`. Heart rate uses `start_datetime/end_datetime`; others use `start_date/end_date` against `day`. Fixture windows include both endpoints, omitted bounds are unbounded. These boundary/default choices are deterministic local fixture semantics, not verified live edge-case parity. Never invent an id for heart-rate samples. Fixtures retain every supplied metric, including nulls. Scope checks use `workout`, `daily`, `heartrate`, `spo2`.
36
42
 
37
43
  Seed `records` rows `{key,userId,collection,data}`. Keys identify storage rows, not vendor ids; distinct keys can deliberately carry duplicate provider data. `pageSize` controls deterministic insertion-order pages. Consumers must retain the same collection/window while paginating; the emulator's row-key cursors do not bind or validate the query. No promised vendor cursor format/order. Set `tokenTtlSeconds` for newly issued tokens.
@@ -44,14 +50,16 @@ Standard `/__admin/health`, `/state`, `/reset`, `/requests`, `/clock`, Timeline
44
50
 
45
51
  Contract: [official Oura OpenAPI 1.41](https://cloud.ouraring.com/v2/static/json/openapi-1.41.json) and [OAuth documentation](https://cloud.ouraring.com/docs/authentication). Synthetic invalid credentials were checked live: token endpoint returned HTTP 400 `invalid_client`, collection endpoint HTTP 401 with the documented detail. Authorized collection data and refresh were not checked live.
46
52
 
53
+ **Best guess, not oracle-verified:** the authorization endpoint and `personal_info` are not observed live, and the vendor OpenAPI JSON was not downloadable (HTTP 404) when this was added. The parameters, the all-scopes default and the single-registered-URI default follow the [Oura authentication page](https://cloud.ouraring.com/docs/authentication). The redirect status (302), the `error`/`error_description`/`state` redirect shape, the statuses and bodies for an unknown client or unregistered `redirect_uri`, the `invalid_scope` allow-list, S256-only PKCE, `access_denied`/`server_error` for consent controls and the 403 for a missing `personal` scope are the emulator's choices, not observed vendor responses. The `personal_info` fields (`id`, `age`, `weight`, `height`, `biological_sex`, `email`) come from the Oura v2 API documentation and third-party client documentation; whether the live response carries `id` is unconfirmed.
54
+
47
55
  `bun test` runs acceptance and every-operation self-parity including divergence detection. `bun run parity` needs `OURA_ACCESS_TOKEN`, performs safe read-only envelope checks, and never prints health data or credentials.
48
56
 
49
57
  ## Deliberately not modelled
50
58
 
51
- No real health data, account signup, consent UI, device pairing, sensor simulation, hardware SDK, webhooks, subscription lifecycle, field projection, statistical computation or full vendor metric validation. Records are synthetic pass-through fixtures; partial metrics can be seeded deliberately. Date boundary/default behavior, cursor internals and quota-body details are local stand-ins, not claims of exact vendor edge-case parity. No Python runtime dependency or claim that the private consumer itself was exercised.
59
+ No real health data, account signup, a consent page (the `consents` rows stand in for the user's answer), device pairing, sensor simulation, hardware SDK, webhooks, subscription lifecycle, field projection, statistical computation or full vendor metric validation. Records are synthetic pass-through fixtures; partial metrics can be seeded deliberately. Date boundary/default behavior, cursor internals and quota-body details are local stand-ins, not claims of exact vendor edge-case parity. No Python runtime dependency or claim that the private consumer itself was exercised.
52
60
 
53
61
  ## API
54
62
 
55
- Root runtime exports: `OuraAPI`, `OURA_NAMESPACE`, `COLLECTION_SCOPES`, `createRuntime`, `OURA_PRESETS`, `document`, `operationIds`, `supportedOperationIds`. Types include `OuraAPIOptions`, `Grant`, `OAuthCode`, `Client`, `DataRecord`, `CollectionName`, `OuraRuntimeOptions`, `OuraRuntime`, `OperationId`, `SupportedOperationId`.
63
+ Root runtime exports: `OuraAPI`, `OURA_NAMESPACE`, `COLLECTION_SCOPES`, `KNOWN_SCOPES`, `createRuntime`, `OURA_PRESETS`, `document`, `operationIds`, `supportedOperationIds`. Types include `OuraAPIOptions`, `Grant`, `OAuthCode`, `Client`, `Consent`, `DataRecord`, `CollectionName`, `OuraRuntimeOptions`, `OuraRuntime`, `OperationId`, `SupportedOperationId`.
56
64
 
57
65
  `/server` exports `createServer`, `serveTarget`, `DEFAULT_PORT` and types `OuraServerOptions`, `OuraServer`.
package/SUPPORT.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Generated from `openapi.yaml`; do not edit by hand.
4
4
 
5
- - operations in spec: **8**
6
- - supported by the emulator: **8**
7
- - parity enabled: **8**
5
+ - operations in spec: **10**
6
+ - supported by the emulator: **10**
7
+ - parity enabled: **10**
8
8
 
9
9
  | operationId | route | emulator | parity | notes |
10
10
  | --- | --- | --- | --- | --- |
@@ -15,4 +15,6 @@ Generated from `openapi.yaml`; do not edit by hand.
15
15
  | `ListDailySpo2` | `GET /v2/usercollection/daily_spo2` | ✅ supported | ✅ | |
16
16
  | `ListDailyReadiness` | `GET /v2/usercollection/daily_readiness` | ✅ supported | ✅ | |
17
17
  | `ListDailySleep` | `GET /v2/usercollection/daily_sleep` | ✅ supported | ✅ | |
18
+ | `GetPersonalInfo` | `GET /v2/usercollection/personal_info` | ✅ supported | ✅ | |
18
19
  | `OAuthToken` | `POST /oauth/token` | ✅ supported | ⚠️ unsafe (opt-in) | |
20
+ | `OAuthAuthorize` | `GET /oauth/authorize` | ✅ supported | ⚠️ unsafe (opt-in) | |
@@ -9,7 +9,7 @@ import {
9
9
  parseFaultHeader,
10
10
  resolveAdminPrefix,
11
11
  restoreClock
12
- } from "./chunk-IK2IQ5CW.js";
12
+ } from "./chunk-ZPLFTMD7.js";
13
13
 
14
14
  // ../../adapters/node/dist/cli.js
15
15
  import { readFile as readFile5 } from "node:fs/promises";
@@ -3057,7 +3057,7 @@ async function preflightFleet(config, options) {
3057
3057
  break;
3058
3058
  }
3059
3059
  }
3060
- targets.set(name, { ...target, packageVersion: packageVersion ?? "1.7.0" });
3060
+ targets.set(name, { ...target, packageVersion: packageVersion ?? "1.8.0" });
3061
3061
  } catch (error) {
3062
3062
  throw new Error(`service ${name}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
3063
3063
  }
@@ -3283,7 +3283,11 @@ var serve = async (api, options = {}) => {
3283
3283
  // ../../adapters/node/dist/listen.js
3284
3284
  var listen = async (api, options = {}) => {
3285
3285
  const host = options.host ?? "127.0.0.1";
3286
- const server = await serve(api, { port: options.port ?? 0, host });
3286
+ const server = await serve(api, {
3287
+ port: options.port ?? 0,
3288
+ host,
3289
+ ...options.maxBodyBytes !== void 0 ? { maxBodyBytes: options.maxBodyBytes } : {}
3290
+ });
3287
3291
  const address = server.address();
3288
3292
  const port = typeof address === "object" && address !== null ? address.port : options.port ?? 0;
3289
3293
  const shown = host.includes(":") ? `[${host}]` : host;
@@ -3482,7 +3486,8 @@ var httpChild = async (target, entry, options, admission, generation, privateAcc
3482
3486
  try {
3483
3487
  server = await listen(api, {
3484
3488
  port: entry.port ?? target.defaultPort,
3485
- host: entry.host ?? "127.0.0.1"
3489
+ host: entry.host ?? "127.0.0.1",
3490
+ ...target.maxBodyBytes !== void 0 ? { maxBodyBytes: target.maxBodyBytes } : {}
3486
3491
  });
3487
3492
  target.listening?.(server);
3488
3493
  } catch (error) {
@@ -4873,7 +4878,11 @@ var start = async (target, values, config) => {
4873
4878
  });
4874
4879
  if (config.idBase !== void 0)
4875
4880
  runtime.setIdBase?.(config.idBase);
4876
- const listening = await listen(runtime, { port: config.port, host: config.host });
4881
+ const listening = await listen(runtime, {
4882
+ port: config.port,
4883
+ host: config.host,
4884
+ ...target.maxBodyBytes !== void 0 ? { maxBodyBytes: target.maxBodyBytes } : {}
4885
+ });
4877
4886
  target.listening?.(listening);
4878
4887
  console.log(`${target.name} emulator listening on ${listening.url}`);
4879
4888
  console.log(`${target.name} health: GET ${listening.url}${adminPrefix}/health`);
@@ -5002,4 +5011,4 @@ export {
5002
5011
  createServer2 as createServer,
5003
5012
  serveTarget
5004
5013
  };
5005
- //# sourceMappingURL=chunk-ZMBSHFPY.js.map
5014
+ //# sourceMappingURL=chunk-NXZZD2JL.js.map