@centia-io/sdk 0.2.12 → 0.2.14

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
@@ -115,6 +115,33 @@ await flow.signIn();
115
115
  flow.signOut(); // Clears tokens/options in local storage (no redirect)
116
116
  ```
117
117
 
118
+ ### GuestFlow (Guest tokens – anonymous access)
119
+
120
+ Obtain tokens for a database's default user (the sub-user used for anonymous access) without any user credentials. Useful for public apps that need a bearer token for public data. Requires that the database has a default user; `clientSecret` is only needed when the OAuth client is not public.
121
+
122
+ Required options:
123
+ - `host`
124
+ - `clientId`
125
+ - `database`
126
+ - `clientSecret` (only for confidential clients)
127
+
128
+ Example:
129
+ ```ts
130
+ import { GuestFlow } from "@centia-io/sdk";
131
+
132
+ const flow = new GuestFlow({
133
+ host: "https://api.centia.io",
134
+ clientId: "your-client-id",
135
+ database: "your-database"
136
+ });
137
+
138
+ await flow.signIn();
139
+ // Tokens for the default user are now stored; subsequent Sql/Rpc calls
140
+ // include the Authorization header and refresh works as usual.
141
+
142
+ flow.signOut(); // Clears tokens/options in local storage (no redirect)
143
+ ```
144
+
118
145
  ### SignUp (Browser – Create a new user)
119
146
 
120
147
  Use this helper in browser applications to redirect the user to the Centia‑io sign‑up page.
@@ -474,6 +501,36 @@ const job = await mapcache.deleteMapcacheTileset('my_database', 'my_schema.roads
474
501
 
475
502
  The endpoint accepts anonymous, HTTP Basic and Bearer token requests; tile requests are authorized against the tileset's layer. Authorization travels in the `Authorization` header — it cannot be embedded in the URL — so for protected tilesets, inject the header per tile request via the map library's request hook (e.g. MapLibre's `transformRequest` or OpenLayers' `tileLoadFunction`).
476
503
 
504
+ ## OGC API (Features / Maps)
505
+
506
+ `Ogc` wraps the RESTful OGC API under `/api/v4/ogc/database/{database}` — OGC API Features (Part 1 Core, Part 2 CRS) for reading features as GeoJSON and OGC API Maps (Part 1 Core) for rendered images. Like `Ows`/`Wfs` it takes a `CentiaHttpClient`; Bearer token, HTTP Basic and anonymous requests are all accepted:
507
+
508
+ ```ts
509
+ import { createCentiaClient, Ogc, OGC_CRS84, ogcEpsgCrs } from '@centia-io/sdk'
510
+
511
+ const ogc = new Ogc(http)
512
+
513
+ const { collections } = await ogc.getCollections('my_database')
514
+ const collection = await ogc.getCollection('my_database', 'my_schema.roads') // extent, crs list, links
515
+
516
+ // GeoJSON items — default page size is 10; follow the `next` link or pass offset
517
+ const page = await ogc.getItems<{ gid: number; name: string }>('my_database', 'my_schema.roads', {
518
+ bbox: [9, 55, 10, 56], // lon/lat in CRS84 (the default bbox-crs)
519
+ crs: ogcEpsgCrs(25832), // output CRS from the collection's crs list
520
+ limit: 100,
521
+ datetime: '2024-01-01T00:00:00Z', // versioned layers: the version valid at that time
522
+ })
523
+ page.numberMatched // total; page.numberReturned; page.links (next/prev)
524
+
525
+ const feature = await ogc.getItem('my_database', 'my_schema.roads', 42)
526
+
527
+ // Map images are fetched by URL (e.g. an <img> or a map library)
528
+ const url = ogc.mapUrl('my_database', 'my_schema.roads', { bbox: [9, 55, 10, 56], width: 512, format: 'png' })
529
+ const multi = ogc.datasetMapUrl('my_database', ['my_schema.roads', 'my_schema.buildings'], { width: 512 })
530
+ ```
531
+
532
+ CRS values are URIs: `OGC_CRS84` (lon/lat, default) or `ogcEpsgCrs(code)`; note `ogcEpsgCrs(4326)` is lat/lon order per OGC API Features Part 2. A collection that exists but is not readable answers `401` (anonymous) or `403` (no privilege); unknown collections and features are `404` — all thrown as `CentiaApiError`. Geofence rules, versioning and workflow are applied server-side.
533
+
477
534
  ## Key/value store
478
535
 
479
536
  `Keyvalue` wraps the `/api/v4/keyvalue` endpoints for storing arbitrary JSON under globally unique keys. It takes a `CentiaHttpClient` and requires a Bearer token:
@@ -341,6 +341,9 @@ var Gc2Service = class {
341
341
  isSignUpOptions(options) {
342
342
  return "parentDb" in options;
343
343
  }
344
+ isGuestFlowOptions(options) {
345
+ return "database" in options && !("username" in options);
346
+ }
344
347
  buildUrl(path) {
345
348
  if (path.startsWith("http://") || path.startsWith("https://")) return path;
346
349
  return `${this.host}${path}`;
@@ -453,12 +456,24 @@ var Gc2Service = class {
453
456
  database
454
457
  });
455
458
  }
456
- async getRefreshToken(token) {
459
+ async getGuestToken() {
457
460
  var _this5 = this;
458
- var _this$options$tokenUr3;
459
- const path = (_this$options$tokenUr3 = _this5.options.tokenUri) !== null && _this$options$tokenUr3 !== void 0 ? _this$options$tokenUr3 : `${_this5.host}/api/v4/oauth`;
461
+ let database;
462
+ if (_this5.isGuestFlowOptions(_this5.options)) database = _this5.options.database;
463
+ else throw new Error("GuestFlow options required for this operation");
464
+ const path = `${_this5.host}/api/v4/oauth/guest`;
460
465
  return _this5.request(_this5.buildUrl(path), "POST", {
466
+ database,
461
467
  client_id: _this5.options.clientId,
468
+ client_secret: _this5.options.clientSecret
469
+ });
470
+ }
471
+ async getRefreshToken(token) {
472
+ var _this6 = this;
473
+ var _this$options$tokenUr3;
474
+ const path = (_this$options$tokenUr3 = _this6.options.tokenUri) !== null && _this$options$tokenUr3 !== void 0 ? _this$options$tokenUr3 : `${_this6.host}/api/v4/oauth`;
475
+ return _this6.request(_this6.buildUrl(path), "POST", {
476
+ client_id: _this6.options.clientId,
462
477
  grant_type: "refresh_token",
463
478
  refresh_token: token
464
479
  });
@@ -576,6 +591,48 @@ var PasswordFlow = class {
576
591
  }
577
592
  };
578
593
 
594
+ //#endregion
595
+ //#region src/GuestFlow.ts
596
+ /**
597
+ * @author Martin Høgh <mh@mapcentia.com>
598
+ * @copyright 2013-2026 MapCentia ApS
599
+ * @license https://opensource.org/license/mit The MIT License
600
+ *
601
+ */
602
+ /**
603
+ * Guest token flow. Issues access/refresh tokens for the database's default
604
+ * user (the sub-user used for anonymous access) without any user credentials.
605
+ * `clientSecret` is only required when the OAuth client is not public.
606
+ */
607
+ var GuestFlow = class {
608
+ constructor(options) {
609
+ this.options = options;
610
+ this.service = new Gc2Service(options);
611
+ }
612
+ async signIn() {
613
+ var _this = this;
614
+ const { access_token, refresh_token } = await _this.service.getGuestToken();
615
+ setTokens({
616
+ accessToken: access_token,
617
+ refreshToken: refresh_token
618
+ });
619
+ setOptions({
620
+ clientId: _this.options.clientId,
621
+ host: _this.options.host,
622
+ redirectUri: "",
623
+ clientSecret: _this.options.clientSecret
624
+ });
625
+ }
626
+ signOut() {
627
+ this.clear();
628
+ }
629
+ clear() {
630
+ clearTokens();
631
+ clearOptions();
632
+ clearNonce();
633
+ }
634
+ };
635
+
579
636
  //#endregion
580
637
  //#region src/http/errors.ts
581
638
  /**
@@ -3041,6 +3098,157 @@ var Mapcache = class {
3041
3098
  }
3042
3099
  };
3043
3100
 
3101
+ //#endregion
3102
+ //#region src/ogc/Ogc.ts
3103
+ /** Default CRS of the OGC API: WGS 84 in longitude/latitude order. */
3104
+ const OGC_CRS84 = "http://www.opengis.net/def/crs/OGC/1.3/CRS84";
3105
+ /**
3106
+ * CRS URI for an EPSG code, e.g. `ogcEpsgCrs(25832)`. Note that
3107
+ * `ogcEpsgCrs(4326)` is latitude/longitude order; use `OGC_CRS84` for lon/lat.
3108
+ */
3109
+ function ogcEpsgCrs(epsg) {
3110
+ return `http://www.opengis.net/def/crs/EPSG/0/${epsg}`;
3111
+ }
3112
+ function bboxToString(bbox) {
3113
+ return typeof bbox === "string" ? bbox : bbox.join(",");
3114
+ }
3115
+ function spatialQuery(options) {
3116
+ const query = {};
3117
+ if ((options === null || options === void 0 ? void 0 : options.bbox) !== void 0) query.bbox = bboxToString(options.bbox);
3118
+ if ((options === null || options === void 0 ? void 0 : options.bboxCrs) !== void 0) query["bbox-crs"] = options.bboxCrs;
3119
+ if ((options === null || options === void 0 ? void 0 : options.crs) !== void 0) query.crs = options.crs;
3120
+ if ((options === null || options === void 0 ? void 0 : options.datetime) !== void 0) query.datetime = options.datetime;
3121
+ return query;
3122
+ }
3123
+ function nonEmpty(query) {
3124
+ return Object.keys(query).length > 0 ? query : void 0;
3125
+ }
3126
+ /**
3127
+ * OGC API Features (Part 1 Core, Part 2 CRS) and OGC API Maps (Part 1 Core)
3128
+ * wrapper for `/api/v4/ogc/database/{database}`.
3129
+ *
3130
+ * The endpoints accept Bearer token, HTTP Basic and anonymous requests. A
3131
+ * Bearer token must match the `database` in the path. Anonymous callers read
3132
+ * layers below `Read/write`; a `Read/write` collection answers 401 (Basic
3133
+ * challenge) for anonymous callers and 403 for identities without privilege.
3134
+ * Geofence rules, versioning and workflow are applied server-side.
3135
+ *
3136
+ * Map images cannot be fetched through this client; use `mapUrl` /
3137
+ * `datasetMapUrl` to build image URLs for `<img>` tags or map libraries.
3138
+ *
3139
+ * ```ts
3140
+ * const ogc = new Ogc(createCentiaClient({ baseUrl, auth }));
3141
+ * const page = await ogc.getItems('my_database', 'my_schema.roads', { bbox: [9, 55, 10, 56], limit: 100 });
3142
+ * ```
3143
+ */
3144
+ var Ogc = class {
3145
+ constructor(client) {
3146
+ this.client = client;
3147
+ }
3148
+ basePath(database) {
3149
+ return `api/v4/ogc/database/${encodeURIComponent(database)}`;
3150
+ }
3151
+ collectionPath(database, collectionId) {
3152
+ return `${this.basePath(database)}/collections/${encodeURIComponent(collectionId)}`;
3153
+ }
3154
+ /** The landing page with links to conformance, collections and the OpenAPI document. */
3155
+ async getLandingPage(database) {
3156
+ var _this = this;
3157
+ return _this.client.request({
3158
+ path: _this.basePath(database),
3159
+ method: "GET"
3160
+ });
3161
+ }
3162
+ /** Conformance classes implemented by the server. */
3163
+ async getConformance(database) {
3164
+ var _this2 = this;
3165
+ return _this2.client.request({
3166
+ path: `${_this2.basePath(database)}/conformance`,
3167
+ method: "GET"
3168
+ });
3169
+ }
3170
+ /** The collections (OWS-enabled layers) the caller may read, paged. */
3171
+ async getCollections(database, options) {
3172
+ var _this3 = this;
3173
+ const query = {};
3174
+ if ((options === null || options === void 0 ? void 0 : options.limit) !== void 0) query.limit = String(options.limit);
3175
+ if ((options === null || options === void 0 ? void 0 : options.offset) !== void 0) query.offset = String(options.offset);
3176
+ return _this3.client.request({
3177
+ path: `${_this3.basePath(database)}/collections`,
3178
+ method: "GET",
3179
+ query: nonEmpty(query)
3180
+ });
3181
+ }
3182
+ /**
3183
+ * One collection. Throws a `CentiaApiError` with status 404 (unknown),
3184
+ * 401 (anonymous caller, credentials required) or 403 (no privilege).
3185
+ */
3186
+ async getCollection(database, collectionId) {
3187
+ var _this4 = this;
3188
+ return _this4.client.request({
3189
+ path: _this4.collectionPath(database, collectionId),
3190
+ method: "GET"
3191
+ });
3192
+ }
3193
+ /**
3194
+ * Features of a collection as a GeoJSON FeatureCollection page. The default
3195
+ * page size is 10; follow `links` with `rel: 'next'` or pass `offset`.
3196
+ */
3197
+ async getItems(database, collectionId, options) {
3198
+ var _this5 = this;
3199
+ const query = {};
3200
+ if ((options === null || options === void 0 ? void 0 : options.limit) !== void 0) query.limit = String(options.limit);
3201
+ if ((options === null || options === void 0 ? void 0 : options.offset) !== void 0) query.offset = String(options.offset);
3202
+ Object.assign(query, spatialQuery(options));
3203
+ return _this5.client.request({
3204
+ path: `${_this5.collectionPath(database, collectionId)}/items`,
3205
+ method: "GET",
3206
+ query: nonEmpty(query),
3207
+ accept: "application/geo+json"
3208
+ });
3209
+ }
3210
+ /** One feature by primary key. Throws a 404 `CentiaApiError` when it does not exist. */
3211
+ async getItem(database, collectionId, featureId, options) {
3212
+ var _this6 = this;
3213
+ const query = {};
3214
+ if ((options === null || options === void 0 ? void 0 : options.crs) !== void 0) query.crs = options.crs;
3215
+ if ((options === null || options === void 0 ? void 0 : options.datetime) !== void 0) query.datetime = options.datetime;
3216
+ return _this6.client.request({
3217
+ path: `${_this6.collectionPath(database, collectionId)}/items/${encodeURIComponent(String(featureId))}`,
3218
+ method: "GET",
3219
+ query: nonEmpty(query),
3220
+ accept: "application/geo+json"
3221
+ });
3222
+ }
3223
+ /**
3224
+ * URL of a rendered map of one collection (PNG/JPEG), without fetching it.
3225
+ * Authorization travels in the `Authorization` header, so for protected
3226
+ * collections fetch the image with the header set (or use Basic auth).
3227
+ */
3228
+ mapUrl(database, collectionId, options) {
3229
+ return this.buildUrl(`${this.collectionPath(database, collectionId)}/map`, this.mapQuery(options));
3230
+ }
3231
+ /** URL of a rendered map of several collections of the same schema. */
3232
+ datasetMapUrl(database, collections, options) {
3233
+ const query = _objectSpread2({ collections: collections.join(",") }, this.mapQuery(options));
3234
+ return this.buildUrl(`${this.basePath(database)}/map`, query);
3235
+ }
3236
+ mapQuery(options) {
3237
+ const query = spatialQuery(options);
3238
+ if ((options === null || options === void 0 ? void 0 : options.width) !== void 0) query.width = String(options.width);
3239
+ if ((options === null || options === void 0 ? void 0 : options.height) !== void 0) query.height = String(options.height);
3240
+ if ((options === null || options === void 0 ? void 0 : options.format) !== void 0) query.f = options.format;
3241
+ if ((options === null || options === void 0 ? void 0 : options.transparent) !== void 0) query.transparent = String(options.transparent);
3242
+ if ((options === null || options === void 0 ? void 0 : options.bgcolor) !== void 0) query.bgcolor = options.bgcolor;
3243
+ return query;
3244
+ }
3245
+ buildUrl(path, query) {
3246
+ let url = `${this.client.baseUrl}/${path}`;
3247
+ if (Object.keys(query).length > 0) url += `?${new URLSearchParams(query).toString()}`;
3248
+ return url;
3249
+ }
3250
+ };
3251
+
3044
3252
  //#endregion
3045
3253
  //#region src/keyvalue/Keyvalue.ts
3046
3254
  /**
@@ -3305,10 +3513,13 @@ exports.Claims = Claims;
3305
3513
  exports.CodeFlow = CodeFlow;
3306
3514
  exports.Features = Features;
3307
3515
  exports.Gql = Gql;
3516
+ exports.GuestFlow = GuestFlow;
3308
3517
  exports.Keyvalue = Keyvalue;
3309
3518
  exports.Mapcache = Mapcache;
3310
3519
  exports.Meta = Meta;
3311
3520
  exports.NotLoggedInError = NotLoggedInError;
3521
+ exports.OGC_CRS84 = OGC_CRS84;
3522
+ exports.Ogc = Ogc;
3312
3523
  exports.Ows = Ows;
3313
3524
  exports.PasswordFlow = PasswordFlow;
3314
3525
  exports.Rpc = Rpc;
@@ -3327,4 +3538,5 @@ exports.createCentiaAdminClient = createCentiaAdminClient;
3327
3538
  exports.createCentiaClient = createCentiaClient;
3328
3539
  exports.createSqlBuilder = createSqlBuilder;
3329
3540
  exports.createTokenProvider = createTokenProvider;
3330
- exports.isCentiaApiError = isCentiaApiError;
3541
+ exports.isCentiaApiError = isCentiaApiError;
3542
+ exports.ogcEpsgCrs = ogcEpsgCrs;