@webpieces/ipc-bridge 0.4.878 → 0.4.880

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
@@ -17,11 +17,13 @@ abstract class PlaybackApi {
17
17
  @WpIpcEndpoint('play.v1')
18
18
  abstract play(request: PlayRequest): Promise<void>;
19
19
  }
20
- const clients = new IpcClientFactory(connection, logging);
20
+ const clients = new IpcClientFactory(connection, logging, ClientRole.END_USER_CLIENT);
21
21
  const playback = clients.createClient(PlaybackApi);
22
22
  await playback.play(new PlayRequest('track-id'));
23
23
  ```
24
24
 
25
+ The third `IpcClientFactory` argument is the `ClientRole` (from `@webpieces/core-util/ipc`), required with no default: `END_USER_CLIENT` when this side acts for a person (a WebView or app shell), so an `unauthorized` reply decodes as `ApiUnauthorizedError` ("log in again"); `SERVER` when the credential this side presents is its own, so the same reply is its own bug (`ApiImplementationError`). See `docs/portable-ipc-and-errors.md`.
26
+
25
27
  The DTO, IDs and metadata live in the shared API package. The names of the abstract class and controller are never sent on the wire. Proxy inspection, symbols and `then` do not send requests. Added JSON fields are naturally ignored by typed consumers; missing fields may fail in application logic.
26
28
 
27
29
  Construct `IpcConnection` with an `IpcTransport` adapter and `IpcConnectionOptions`: a positive timeout, a connection-wide unique ID generator, a scheduler returning timer cancellation functions, and an error owner. The adapter sends strings and subscribes once to incoming strings, transport errors and close events. Its send Promise must reject on send failure; parse/message-error callbacks go to the failure subscriber. A vendor bus adapter must not expose vendor Node declarations through its public contract. Do not install a second vendor handler for every API.
@@ -30,7 +32,7 @@ Construct `IpcConnection` with an `IpcTransport` adapter and `IpcConnectionOptio
30
32
 
31
33
  Use `clients.withContext(inboundContext)` when making nested calls; this copies the transaction and makes the inbound call the parent. No ambient mutable state is held across awaits. `createScoped` on the server supports constructing a controller with a scoped client factory.
32
34
 
33
- Timeout rejects with the existing `ApiCallTimeoutError`. Send/close/disposal rejects with local `IpcTransportError`, preserving the original failure as standard `cause`. This extends `Error`, not semantic `ApiError`, and is never a remote wire category. A receiver throwing `ApiUnavailableError` still arrives as that canonical semantic error, so callers can distinguish a failed local bridge from an unavailable remote implementation. A timeout does not cancel remote side effects, and the framework never replays requests. `dispose()` rejects pending requests and closes the adapter. Late replies are reported and ignored. Malformed envelopes and correlation mismatches fail the connection, including its pending calls. Observe all OS-callback send Promises or route rejections to the application's explicit error owner.
35
+ Timeout rejects with the existing `ApiCallTimeoutError`; its `context.operation` is `WRITE` (IPC contracts declare no operation), so its `userMessage` tells the user to check before repeating the call. Send/close/disposal rejects with local `IpcTransportError`, preserving the original failure as standard `cause`. This extends `Error`, not semantic `ApiError`, and is never a remote wire category. A receiver throwing `ApiUnavailableError` still arrives as that canonical semantic error, so callers can distinguish a failed local bridge from an unavailable remote implementation. A timeout does not cancel remote side effects, and the framework never replays requests. `dispose()` rejects pending requests and closes the adapter. Late replies are reported and ignored. Malformed envelopes and correlation mismatches fail the connection, including its pending calls. Observe all OS-callback send Promises or route rejections to the application's explicit error owner.
34
36
 
35
37
  Host responsibilities: validate WebView origins/navigation/session identity and expose only authorized APIs. Suspension/resume, coalescing native state, durable usage journals and installed shell protocol minimums are application policies. Generic IPC cannot make suspended website JavaScript execute. Android/iOS lifecycle and locked-screen tests remain downstream integration work.
36
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/ipc-bridge",
3
- "version": "0.4.878",
3
+ "version": "0.4.880",
4
4
  "description": "Portable typed duplex IPC client and server factories for browser, React Native and Node",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -22,6 +22,6 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@webpieces/core-util": "0.4.878"
25
+ "@webpieces/core-util": "0.4.880"
26
26
  }
27
27
  }
@@ -1,10 +1,18 @@
1
- import { IpcCallContext, IpcConnection, IpcApiType, IpcLogging } from '@webpieces/core-util/ipc';
1
+ import { ClientRole, IpcCallContext, IpcConnection, IpcApiType, IpcLogging } from '@webpieces/core-util/ipc';
2
2
  /** Typed proxies on one trusted duplex connection; no HTTP decorators or container required. */
3
3
  export declare class IpcClientFactory {
4
4
  private readonly connection;
5
5
  private readonly logging;
6
+ private readonly role;
6
7
  private readonly parent?;
7
- constructor(connection: IpcConnection, logging: IpcLogging, parent?: IpcCallContext | undefined);
8
+ /**
9
+ * @param role - WHO receives this factory's replies (see {@link ClientRole}). REQUIRED, with no
10
+ * default (#1173): it decides what an `unauthorized` reply means. An end-user client (the
11
+ * WebView or app shell acting for the person) decodes it as `ApiUnauthorizedError` ("log in
12
+ * again"); a server decodes it as `ApiImplementationError`, because the credential it presented
13
+ * was its own.
14
+ */
15
+ constructor(connection: IpcConnection, logging: IpcLogging, role: ClientRole, parent?: IpcCallContext | undefined);
8
16
  /** Explicit scope propagation is safe across concurrent async calls in browser and RN. */
9
17
  withContext(context: IpcCallContext): IpcClientFactory;
10
18
  createClient<T extends object>(apiClass: IpcApiType<T>): T;
@@ -7,15 +7,24 @@ const ipc_1 = require("@webpieces/core-util/ipc");
7
7
  class IpcClientFactory {
8
8
  connection;
9
9
  logging;
10
+ role;
10
11
  parent;
11
- constructor(connection, logging, parent) {
12
+ /**
13
+ * @param role - WHO receives this factory's replies (see {@link ClientRole}). REQUIRED, with no
14
+ * default (#1173): it decides what an `unauthorized` reply means. An end-user client (the
15
+ * WebView or app shell acting for the person) decodes it as `ApiUnauthorizedError` ("log in
16
+ * again"); a server decodes it as `ApiImplementationError`, because the credential it presented
17
+ * was its own.
18
+ */
19
+ constructor(connection, logging, role, parent) {
12
20
  this.connection = connection;
13
21
  this.logging = logging;
22
+ this.role = role;
14
23
  this.parent = parent;
15
24
  }
16
25
  /** Explicit scope propagation is safe across concurrent async calls in browser and RN. */
17
26
  withContext(context) {
18
- return new IpcClientFactory(this.connection, this.logging, context);
27
+ return new IpcClientFactory(this.connection, this.logging, this.role, context);
19
28
  }
20
29
  createClient(apiClass) {
21
30
  const apiId = (0, ipc_1.assertInternalApi)(apiClass);
@@ -33,7 +42,7 @@ class IpcClientFactory {
33
42
  // EVERY reply passes the seam, success included, so an app can turn an
34
43
  // apparently-successful reply into a throw. `asserts reply is IpcSuccess`
35
44
  // is what leaves no `type === 'failure'` branch behind here.
36
- ipc_1.IpcClientErrorTranslator.throwIfFailure(reply);
45
+ ipc_1.IpcClientErrorTranslator.throwIfFailure(reply, this.role);
37
46
  return reply.body === null ? undefined : reply.body;
38
47
  });
39
48
  });
@@ -1 +1 @@
1
- {"version":3,"file":"IpcClientFactory.js","sourceRoot":"","sources":["../../../../../packages/core/ipc-bridge/src/IpcClientFactory.ts"],"names":[],"mappings":";;;AAAA,wDAAqE;AACrE,kDAYkC;AAElC,gGAAgG;AAChG,MAAa,gBAAgB;IAEJ;IACA;IACA;IAHrB,YACqB,UAAyB,EACzB,OAAmB,EACnB,MAAuB;QAFvB,eAAU,GAAV,UAAU,CAAe;QACzB,YAAO,GAAP,OAAO,CAAY;QACnB,WAAM,GAAN,MAAM,CAAiB;IACzC,CAAC;IAEJ,0FAA0F;IAC1F,WAAW,CAAC,OAAuB;QAC/B,OAAO,IAAI,gBAAgB,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACxE,CAAC;IAED,YAAY,CAAmB,QAAuB;QAClD,MAAM,KAAK,GAAG,IAAA,uBAAiB,EAAC,QAAQ,CAAC,CAAC;QAC1C,MAAM,SAAS,GAAG,IAAA,qBAAe,EAAC,QAAQ,CAAC,CAAC;QAC5C,0IAA0I;QAC1I,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuD,CAAC;QAC/E,KAAK,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YACtD,0IAA0I;YAC1I,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,OAAgB,EAAoB,EAAE;gBAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBACxD,OAAO,mBAAa,CAAC,OAAO,CACxB,IAAI,CAAC,OAAO,EACZ,OAAO,EACP,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,IAAA,oBAAc,EAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,IAAI,cAAQ,CAAC,EAAE,CAAC,EACjD,OAAO,EACP,KAAK,IAAI,EAAE;oBACP,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS;wBACzC,MAAM,IAAI,+BAAsB,CAC5B,uCAAuC,CAC1C,CAAC;oBACN,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CACvC,IAAI,gBAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CACpD,CAAC;oBACF,uEAAuE;oBACvE,0EAA0E;oBAC1E,6DAA6D;oBAC7D,8BAAwB,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;oBAC/C,OAAO,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;gBACxD,CAAC,CACJ,CAAC;YACN,CAAC,CAAC,CAAC;QACP,CAAC;QACD,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAM,EAAE,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7E,CAAC;CACJ;AAhDD,4CAgDC;AAED,MAAM,eAAe;IAGI;IAFrB;IACI,0IAA0I;IACzH,OAAyE;QAAzE,YAAO,GAAP,OAAO,CAAkE;IAC3F,CAAC;IACJ,0IAA0I;IAC1I,GAAG,CAAC,OAAU,EAAE,GAAoB;QAChC,8FAA8F;QAC9F,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IACD,GAAG,CAAC,OAAU,EAAE,GAAoB;QAChC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;CACJ","sourcesContent":["import { ApiImplementationError } from '@webpieces/core-util/errors';\nimport {\n IpcCallContext,\n IpcClientErrorTranslator,\n IpcConnection,\n IpcApiType,\n IpcLogging,\n IpcRequest,\n IpcCallLogger,\n assertInternalApi,\n getIpcEndpoints,\n getIpcMaskSpec,\n MaskSpec,\n} from '@webpieces/core-util/ipc';\n\n/** Typed proxies on one trusted duplex connection; no HTTP decorators or container required. */\nexport class IpcClientFactory {\n constructor(\n private readonly connection: IpcConnection,\n private readonly logging: IpcLogging,\n private readonly parent?: IpcCallContext,\n ) {}\n\n /** Explicit scope propagation is safe across concurrent async calls in browser and RN. */\n withContext(context: IpcCallContext): IpcClientFactory {\n return new IpcClientFactory(this.connection, this.logging, context);\n }\n\n createClient<T extends object>(apiClass: IpcApiType<T>): T {\n const apiId = assertInternalApi(apiClass);\n const endpoints = getIpcEndpoints(apiClass);\n // webpieces-disable no-any-unknown -- untrusted IPC data is schema-validated; generic dispatch cannot assume a DTO type before validation\n const methods = new Map<PropertyKey, (request: unknown) => Promise<unknown>>();\n for (const [key, methodId] of Object.entries(endpoints)) {\n // webpieces-disable no-any-unknown -- untrusted IPC data is schema-validated; generic dispatch cannot assume a DTO type before validation\n methods.set(key, async (request: unknown): Promise<unknown> => {\n const context = this.connection.newContext(this.parent);\n return IpcCallLogger.execute(\n this.logging,\n context,\n 'client',\n apiId,\n methodId,\n getIpcMaskSpec(apiClass, key) ?? new MaskSpec({}),\n request,\n async () => {\n if (request === null || request === undefined)\n throw new ApiImplementationError(\n 'IPC requests require one non-null DTO',\n );\n const reply = await this.connection.request(\n new IpcRequest(apiId, methodId, context, request),\n );\n // EVERY reply passes the seam, success included, so an app can turn an\n // apparently-successful reply into a throw. `asserts reply is IpcSuccess`\n // is what leaves no `type === 'failure'` branch behind here.\n IpcClientErrorTranslator.throwIfFailure(reply);\n return reply.body === null ? undefined : reply.body;\n },\n );\n });\n }\n return new Proxy(Object.create(null) as T, new IpcProxyHandler(methods));\n }\n}\n\nclass IpcProxyHandler<T extends object> implements ProxyHandler<T> {\n constructor(\n // webpieces-disable no-any-unknown -- untrusted IPC data is schema-validated; generic dispatch cannot assume a DTO type before validation\n private readonly methods: ReadonlyMap<PropertyKey, (request: unknown) => Promise<unknown>>,\n ) {}\n // webpieces-disable no-any-unknown -- untrusted IPC data is schema-validated; generic dispatch cannot assume a DTO type before validation\n get(_target: T, key: string | symbol): unknown {\n // Promise assimilation, inspection, symbols and framework instrumentation aren't RPC methods.\n return this.methods.get(key);\n }\n has(_target: T, key: string | symbol): boolean {\n return this.methods.has(key);\n }\n}\n"]}
1
+ {"version":3,"file":"IpcClientFactory.js","sourceRoot":"","sources":["../../../../../packages/core/ipc-bridge/src/IpcClientFactory.ts"],"names":[],"mappings":";;;AAAA,wDAAqE;AACrE,kDAakC;AAElC,gGAAgG;AAChG,MAAa,gBAAgB;IASJ;IACA;IACA;IACA;IAXrB;;;;;;OAMG;IACH,YACqB,UAAyB,EACzB,OAAmB,EACnB,IAAgB,EAChB,MAAuB;QAHvB,eAAU,GAAV,UAAU,CAAe;QACzB,YAAO,GAAP,OAAO,CAAY;QACnB,SAAI,GAAJ,IAAI,CAAY;QAChB,WAAM,GAAN,MAAM,CAAiB;IACzC,CAAC;IAEJ,0FAA0F;IAC1F,WAAW,CAAC,OAAuB;QAC/B,OAAO,IAAI,gBAAgB,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACnF,CAAC;IAED,YAAY,CAAmB,QAAuB;QAClD,MAAM,KAAK,GAAG,IAAA,uBAAiB,EAAC,QAAQ,CAAC,CAAC;QAC1C,MAAM,SAAS,GAAG,IAAA,qBAAe,EAAC,QAAQ,CAAC,CAAC;QAC5C,0IAA0I;QAC1I,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuD,CAAC;QAC/E,KAAK,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YACtD,0IAA0I;YAC1I,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,OAAgB,EAAoB,EAAE;gBAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBACxD,OAAO,mBAAa,CAAC,OAAO,CACxB,IAAI,CAAC,OAAO,EACZ,OAAO,EACP,QAAQ,EACR,KAAK,EACL,QAAQ,EACR,IAAA,oBAAc,EAAC,QAAQ,EAAE,GAAG,CAAC,IAAI,IAAI,cAAQ,CAAC,EAAE,CAAC,EACjD,OAAO,EACP,KAAK,IAAI,EAAE;oBACP,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS;wBACzC,MAAM,IAAI,+BAAsB,CAC5B,uCAAuC,CAC1C,CAAC;oBACN,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CACvC,IAAI,gBAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CACpD,CAAC;oBACF,uEAAuE;oBACvE,0EAA0E;oBAC1E,6DAA6D;oBAC7D,8BAAwB,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC1D,OAAO,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;gBACxD,CAAC,CACJ,CAAC;YACN,CAAC,CAAC,CAAC;QACP,CAAC;QACD,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAM,EAAE,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7E,CAAC;CACJ;AAxDD,4CAwDC;AAED,MAAM,eAAe;IAGI;IAFrB;IACI,0IAA0I;IACzH,OAAyE;QAAzE,YAAO,GAAP,OAAO,CAAkE;IAC3F,CAAC;IACJ,0IAA0I;IAC1I,GAAG,CAAC,OAAU,EAAE,GAAoB;QAChC,8FAA8F;QAC9F,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IACD,GAAG,CAAC,OAAU,EAAE,GAAoB;QAChC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;CACJ","sourcesContent":["import { ApiImplementationError } from '@webpieces/core-util/errors';\nimport {\n ClientRole,\n IpcCallContext,\n IpcClientErrorTranslator,\n IpcConnection,\n IpcApiType,\n IpcLogging,\n IpcRequest,\n IpcCallLogger,\n assertInternalApi,\n getIpcEndpoints,\n getIpcMaskSpec,\n MaskSpec,\n} from '@webpieces/core-util/ipc';\n\n/** Typed proxies on one trusted duplex connection; no HTTP decorators or container required. */\nexport class IpcClientFactory {\n /**\n * @param role - WHO receives this factory's replies (see {@link ClientRole}). REQUIRED, with no\n * default (#1173): it decides what an `unauthorized` reply means. An end-user client (the\n * WebView or app shell acting for the person) decodes it as `ApiUnauthorizedError` (\"log in\n * again\"); a server decodes it as `ApiImplementationError`, because the credential it presented\n * was its own.\n */\n constructor(\n private readonly connection: IpcConnection,\n private readonly logging: IpcLogging,\n private readonly role: ClientRole,\n private readonly parent?: IpcCallContext,\n ) {}\n\n /** Explicit scope propagation is safe across concurrent async calls in browser and RN. */\n withContext(context: IpcCallContext): IpcClientFactory {\n return new IpcClientFactory(this.connection, this.logging, this.role, context);\n }\n\n createClient<T extends object>(apiClass: IpcApiType<T>): T {\n const apiId = assertInternalApi(apiClass);\n const endpoints = getIpcEndpoints(apiClass);\n // webpieces-disable no-any-unknown -- untrusted IPC data is schema-validated; generic dispatch cannot assume a DTO type before validation\n const methods = new Map<PropertyKey, (request: unknown) => Promise<unknown>>();\n for (const [key, methodId] of Object.entries(endpoints)) {\n // webpieces-disable no-any-unknown -- untrusted IPC data is schema-validated; generic dispatch cannot assume a DTO type before validation\n methods.set(key, async (request: unknown): Promise<unknown> => {\n const context = this.connection.newContext(this.parent);\n return IpcCallLogger.execute(\n this.logging,\n context,\n 'client',\n apiId,\n methodId,\n getIpcMaskSpec(apiClass, key) ?? new MaskSpec({}),\n request,\n async () => {\n if (request === null || request === undefined)\n throw new ApiImplementationError(\n 'IPC requests require one non-null DTO',\n );\n const reply = await this.connection.request(\n new IpcRequest(apiId, methodId, context, request),\n );\n // EVERY reply passes the seam, success included, so an app can turn an\n // apparently-successful reply into a throw. `asserts reply is IpcSuccess`\n // is what leaves no `type === 'failure'` branch behind here.\n IpcClientErrorTranslator.throwIfFailure(reply, this.role);\n return reply.body === null ? undefined : reply.body;\n },\n );\n });\n }\n return new Proxy(Object.create(null) as T, new IpcProxyHandler(methods));\n }\n}\n\nclass IpcProxyHandler<T extends object> implements ProxyHandler<T> {\n constructor(\n // webpieces-disable no-any-unknown -- untrusted IPC data is schema-validated; generic dispatch cannot assume a DTO type before validation\n private readonly methods: ReadonlyMap<PropertyKey, (request: unknown) => Promise<unknown>>,\n ) {}\n // webpieces-disable no-any-unknown -- untrusted IPC data is schema-validated; generic dispatch cannot assume a DTO type before validation\n get(_target: T, key: string | symbol): unknown {\n // Promise assimilation, inspection, symbols and framework instrumentation aren't RPC methods.\n return this.methods.get(key);\n }\n has(_target: T, key: string | symbol): boolean {\n return this.methods.has(key);\n }\n}\n"]}