@foir/sdk 0.27.1 → 0.29.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/dist/server.js CHANGED
@@ -144,6 +144,7 @@ var GRANT_TABLE = [
144
144
  ["apikeys:write", "none", [], []],
145
145
  ["organizations:read", "none", [], []],
146
146
  ["organizations:write", "none", [], []],
147
+ ["orgsettings:write", "none", [], []],
147
148
  ["organizations:create", "none", [], []],
148
149
  ["organizations:delete", "none", [], []],
149
150
  ["organizations:reach", "none", [], []],
@@ -154,7 +155,7 @@ var GRANT_TABLE = [
154
155
  ["projects:read", "none", [], []],
155
156
  ["projects:write", "none", [], []],
156
157
  ["project:stand", "none", [], []],
157
- ["project:manage", "none", [], ["project:stand", "records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:setup", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read.all", "notes:write.all", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "auth:login_with_foir", "authclients:read", "authclients:write", "domains:read", "domains:write", "loginpages:read", "loginpages:write", "secrets:read", "secrets:list", "secrets:put", "secrets:delete", "appsecrets:read", "appsecrets:list", "appsecrets:put", "appsecrets:delete", "usersecrets:read", "usersecrets:list", "usersecrets:put", "usersecrets:delete", "opsecrets:read", "opsecrets:write", "settings:read", "settings:write", "designtokens:read", "designtokens:write", "models:read", "models:write", "configs:read", "configs:write", "signinproviders:read", "signinproviders:write", "apps:read", "apps:write", "events:read", "targeting:debug", "visitors:forward", "apikeys:read", "apikeys:write", "organizations:read", "organizations:create", "organizations:delete", "organizations:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
158
+ ["project:manage", "none", [], ["project:stand", "records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:setup", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read.all", "notes:write.all", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "authclients:read", "authclients:write", "domains:read", "domains:write", "loginpages:read", "loginpages:write", "secrets:read", "secrets:list", "secrets:put", "secrets:delete", "appsecrets:read", "appsecrets:list", "appsecrets:put", "appsecrets:delete", "usersecrets:read", "usersecrets:list", "usersecrets:put", "usersecrets:delete", "opsecrets:read", "opsecrets:write", "settings:read", "settings:write", "designtokens:read", "designtokens:write", "models:read", "models:write", "configs:read", "configs:write", "signinproviders:read", "signinproviders:write", "apps:read", "apps:write", "events:read", "targeting:debug", "visitors:forward", "apikeys:read", "apikeys:write", "organizations:read", "organizations:create", "organizations:delete", "organizations:write", "orgsettings:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
158
159
  ["projects:create", "none", [], []],
159
160
  ["projects:delete", "none", [], []],
160
161
  ["tenants:read", "none", [], []],
@@ -185,7 +186,6 @@ var GRANT_TABLE = [
185
186
  ["usersecrets:list", "none", [], []],
186
187
  ["usersecrets:put", "none", [], []],
187
188
  ["usersecrets:delete", "none", [], []],
188
- ["auth:login_with_foir", "none", [], []],
189
189
  ["signinproviders:read", "none", [], []],
190
190
  ["signinproviders:write", "none", [], []]
191
191
  ];
@@ -1107,8 +1107,18 @@ async function resolveFoirAuth(config) {
1107
1107
  key: { apiKey: config.apiKey, bearerToken }
1108
1108
  };
1109
1109
  }
1110
+ var renewing = /* @__PURE__ */ new WeakMap();
1111
+ function renewBearerOnce(refresh) {
1112
+ const running = renewing.get(refresh);
1113
+ if (running) return running;
1114
+ const next = refresh().finally(() => renewing.delete(refresh));
1115
+ renewing.set(refresh, next);
1116
+ return next;
1117
+ }
1110
1118
 
1111
1119
  // src/data/ws-transport.ts
1120
+ var CLOSE_CREDENTIAL_ENDED = 4402;
1121
+ var CLOSE_FORBIDDEN = 4403;
1112
1122
  var GQL = {
1113
1123
  connectionInit: "connection_init",
1114
1124
  connectionAck: "connection_ack",
@@ -1143,6 +1153,9 @@ var FoirSocketTransport = class {
1143
1153
  disposed = false;
1144
1154
  keepAlive = null;
1145
1155
  retryTimer = null;
1156
+ // A bearer renewed after the socket's lapsed (4402), presented from the next
1157
+ // connect on in place of the client's own.
1158
+ renewedBearer = null;
1146
1159
  constructor(config) {
1147
1160
  if (config.dispatch !== void 0) {
1148
1161
  throw new FoirRequestError(
@@ -1215,12 +1228,14 @@ var FoirSocketTransport = class {
1215
1228
  socket.onmessage = (event) => this.receive(event.data);
1216
1229
  socket.onerror = () => {
1217
1230
  };
1218
- socket.onclose = () => this.handleClose();
1231
+ socket.onclose = (event) => this.handleClose(event);
1219
1232
  }
1220
1233
  async sendConnectionInit(socket) {
1221
1234
  this.connecting = false;
1222
1235
  try {
1223
- const { headers } = await resolveFoirAuth(this.config);
1236
+ const { headers } = await resolveFoirAuth(
1237
+ this.renewedBearer ? { ...this.config, bearerToken: this.renewedBearer } : this.config
1238
+ );
1224
1239
  const { "Content-Type": _contentType, ...params } = headers;
1225
1240
  const extra = await this.config.connectionParams?.() ?? {};
1226
1241
  this.send(socket, {
@@ -1234,19 +1249,53 @@ var FoirSocketTransport = class {
1234
1249
  this.teardownSocket(true);
1235
1250
  }
1236
1251
  }
1237
- handleClose() {
1252
+ handleClose(event) {
1253
+ const code = event?.code;
1238
1254
  const wasAcknowledged = this.acknowledged;
1239
1255
  this.teardownSocket(false);
1240
1256
  if (this.disposed || this.registrations.size === 0) return;
1241
1257
  for (const registration of this.registrations.values()) {
1242
1258
  registration.sent = false;
1243
1259
  }
1260
+ if (code === CLOSE_FORBIDDEN) {
1261
+ this.failAll(
1262
+ new FoirRequestError(
1263
+ "[foir] the subscription socket was refused where it asked to stand: the signed-in user is no member of the organization this client names"
1264
+ )
1265
+ );
1266
+ return;
1267
+ }
1268
+ if (code === CLOSE_CREDENTIAL_ENDED) {
1269
+ void this.reconnectWithARenewedBearer();
1270
+ return;
1271
+ }
1244
1272
  this.scheduleReconnect(
1245
1273
  new FoirRequestError(
1246
1274
  wasAcknowledged ? "[foir] the subscription socket closed and could not be re-established" : "[foir] the subscription socket closed before the credential was accepted"
1247
1275
  )
1248
1276
  );
1249
1277
  }
1278
+ /**
1279
+ * Reconnect after the socket's credential lapsed: with the bearer renewed
1280
+ * through the client's `refreshBearer` where it has one, failing every
1281
+ * subscription with `FoirSessionExpiredError` when the session is over. A
1282
+ * client without one reconnects at once, presenting what its bearer
1283
+ * provider gives now.
1284
+ */
1285
+ async reconnectWithARenewedBearer() {
1286
+ const refresh = this.config.refreshBearer;
1287
+ if (refresh) {
1288
+ const token = await renewBearerOnce(refresh).catch(() => null);
1289
+ if (this.disposed) return;
1290
+ if (!token) {
1291
+ this.failAll(new FoirSessionExpiredError());
1292
+ return;
1293
+ }
1294
+ this.renewedBearer = token;
1295
+ }
1296
+ this.attempt = 0;
1297
+ this.ensureConnected();
1298
+ }
1250
1299
  scheduleReconnect(error) {
1251
1300
  if (this.disposed || this.registrations.size === 0) return;
1252
1301
  if (this.attempt >= this.policy.maxAttempts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foir/sdk",
3
- "version": "0.27.1",
3
+ "version": "0.29.0",
4
4
  "description": "Official Foir SDK — editor iframe client + server-side platform helpers",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -173,7 +173,7 @@
173
173
  "react-dom": "^19.2.0",
174
174
  "tsup": "^8.5.1",
175
175
  "typescript": "5.9.2",
176
- "vitest": "^3.2.4",
176
+ "vitest": "^5.0.5",
177
177
  "ws": "^8.18.0",
178
178
  "y-websocket": "^3.0.0",
179
179
  "yjs": "^13.6.30",