@lessly/sdk-app 61.0.2 → 61.2.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.
@@ -1,7 +1,8 @@
1
1
  export { createLesslyApp } from './runtime/createLesslyApp.js';
2
2
  export type { LesslyClientBase } from './runtime/createLesslyApp.js';
3
3
  export { connectStream } from './runtime/connectStream.js';
4
- export { LesslyApiError } from './runtime/errors.js';
4
+ export { LesslyApiError, isAccessDenied } from './runtime/errors.js';
5
5
  export type { LesslyAppOptions, HttpMethod, Binding, BindingsMap, ParamSpec, ParamIn, ToolLevel, Operation, } from './runtime/types.js';
6
6
  export type { WsBinding, WsBindingsMap, LesslyStream, LesslyStreamOpener, StreamCloseInfo, StreamData, WebSocketCtor, WebSocketLike, } from './runtime/types.js';
7
+ export type { Access, AccessState, AccessApi } from './runtime/access.js';
7
8
  export type { GeneratedClient } from './gen/client.gen.js';
@@ -0,0 +1,33 @@
1
+ import type { Access, AccessApi, AccessState } from '../runtime/access.js';
2
+ import type { Operation } from '../runtime/types.js';
3
+ /**
4
+ * The only thing these hooks need from a client — so a test double is one object literal, and
5
+ * (deliberately) no React type appears in any exported signature: `react` is an OPTIONAL peer,
6
+ * so the published declarations must typecheck for a consumer who has not installed it.
7
+ */
8
+ export interface AppLike {
9
+ readonly access: AccessApi;
10
+ }
11
+ export interface UseAccessResult {
12
+ state: AccessState;
13
+ role: string | undefined;
14
+ access: AccessApi;
15
+ reload: () => Promise<Access>;
16
+ }
17
+ /**
18
+ * Subscribe a component to the client's access snapshot, loading it once on mount.
19
+ *
20
+ * `useSyncExternalStore` rather than `useState` + an effect: the snapshot lives outside React
21
+ * (several components share one `AccessApi`), and this is the hook that is safe against tearing
22
+ * under concurrent rendering. `load()` is de-duplicated inside `createAccess`, so every component
23
+ * may call this without coordinating — two components produce one request.
24
+ */
25
+ export declare function useAccess(app: AppLike): UseAccessResult;
26
+ /**
27
+ * True iff the caller is PREDICTED to be allowed to perform `op`. False until the snapshot is
28
+ * ready — so a control wired to this starts disabled and enables itself, never the reverse.
29
+ *
30
+ * Loads on mount through `useAccess`, so a component may use it without the App having called
31
+ * `load()` anywhere. This is a hint for the UI, never authorisation: see APP-012.
32
+ */
33
+ export declare function useCan(app: AppLike, op: Operation | string): boolean;
@@ -0,0 +1,40 @@
1
+ import type { LesslyAppOptions, BindingsMap, Binding, ToolLevel, Operation } from './types.js';
2
+ /** Where the access snapshot is in its lifecycle. `can()` is true-capable only in `ready`. */
3
+ export type AccessState = 'idle' | 'loading' | 'ready' | 'error';
4
+ /** The caller's own membership of the active product, exactly as `/me` returns it (flat). */
5
+ export interface Access {
6
+ role: string;
7
+ roleId: string | null;
8
+ roleName: string | null;
9
+ roleType: 'system' | 'custom' | null;
10
+ allow: string[];
11
+ deny: string[];
12
+ }
13
+ export interface AccessApi {
14
+ /** Fetches once and caches. Concurrent calls share a single request. Rejects on failure. */
15
+ load(): Promise<Access>;
16
+ /** Synchronous PREDICTION of the gateway's answer. False whenever state !== 'ready'. */
17
+ can(op: Operation | string): boolean;
18
+ readonly role: string | undefined;
19
+ readonly state: AccessState;
20
+ /** Drops the cache and returns to `idle`; the next `load()` hits the network. */
21
+ invalidate(): void;
22
+ /** Fires on every state change. Returns an unsubscribe function. */
23
+ subscribe(cb: () => void): () => void;
24
+ }
25
+ /** The catalog tool this whole surface is built on. */
26
+ export declare const ME_OPERATION_KEY = "organization_product_me";
27
+ /**
28
+ * The route to `/me`, as a `Binding`, for the window in which the SDK ships BEFORE the production
29
+ * catalog carries `organization_product_me`. `src/gen` is regenerated from the production catalog
30
+ * and must never be hand-edited, so without this constant `load()` would need a second SDK
31
+ * release to start working after Workspace deploys the endpoint.
32
+ *
33
+ * Rooted exactly as every other `organization_*` tool is in the real `bindings.gen`
34
+ * (`organization_product_list-members` -> `/governance/api/v1/products/:productId/members`). The
35
+ * generated binding WINS whenever it exists — the catalog is the authority, this is the stopgap —
36
+ * and `access.test.ts` pins the constant against `fixtures/catalog.sample.json` so the day the
37
+ * catalog does carry the tool, a divergence fails a test instead of 404-ing in a browser.
38
+ */
39
+ export declare const ME_ROUTE: Binding;
40
+ export declare function createAccess(opts: LesslyAppOptions, bindings: BindingsMap, operations: Record<string, ToolLevel>): AccessApi;
@@ -1,6 +1,10 @@
1
1
  import type { LesslyAppOptions, LesslyStreamOpener } from './types.js';
2
+ import type { AccessApi } from './access.js';
2
3
  import type { GeneratedClient } from '../gen/client.gen.js';
3
4
  export interface LesslyClientBase extends LesslyStreamOpener {
4
5
  call(toolName: string, input?: Record<string, unknown>): Promise<unknown>;
6
+ /** The caller's own access to the active product — a PREDICTION of the gateway's answer,
7
+ * never a substitute for it. See `createAccess`. */
8
+ readonly access: AccessApi;
5
9
  }
6
10
  export declare function createLesslyApp(opts: LesslyAppOptions): GeneratedClient & LesslyClientBase;
@@ -2,6 +2,17 @@ export declare class LesslyApiError extends Error {
2
2
  readonly status: number;
3
3
  readonly code: string | null;
4
4
  readonly body: unknown;
5
+ /** True iff this is the gateway refusing the call over the caller's access. See ACCESS_DENIED_CODES. */
6
+ readonly accessDenied: boolean;
5
7
  constructor(status: number, code: string | null, message: string, body: unknown);
6
8
  }
7
9
  export declare function parseErrorBody(body: unknown, status: number): LesslyApiError;
10
+ /**
11
+ * True iff `err` is the gateway refusing a call over the caller's access.
12
+ *
13
+ * Deliberately structural after the `instanceof` fast path: an App is composed into the shell
14
+ * through Module Federation and `@lessly/sdk-app` is not a shared singleton, so the error a
15
+ * caller catches is routinely an instance of the OTHER copy's class. `instanceof` alone would
16
+ * report false there and an App would render a red crash for an ordinary permission refusal.
17
+ */
18
+ export declare function isAccessDenied(err: unknown): boolean;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * VERBATIM COPY of lessly-workspace `packages/sdk-governance/src/permission-match.ts`.
3
+ *
4
+ * `@lessly/sdk-governance` is private on GAR and `@lessly/sdk-app` is public on npmjs, so this
5
+ * SDK cannot depend on it — and `can()` is only useful if it predicts the gateway's answer with
6
+ * the gateway's own code. Everything below this header is byte-identical to the original.
7
+ *
8
+ * CHANGE ONLY TOGETHER WITH THE ORIGINAL. `permission-match.cases.json` is the shared corpus both
9
+ * repositories run; a change that makes the two files disagree makes `can()` lie.
10
+ */
11
+ /**
12
+ * Pure, segment-aware permission matcher — the single source of truth for RBAC
13
+ * grant semantics.
14
+ *
15
+ * Operation keys are `{slug}_{entity}_{action}` split on `_` (depth-agnostic).
16
+ * `*` is a wildcard only as a complete segment. Interior/leading `*` matches
17
+ * exactly one segment; a trailing `*` matches one-or-more remaining segments.
18
+ */
19
+ /** Coarse sensitivity band an operation can declare. */
20
+ export type ToolLevel = 'read' | 'write' | 'admin';
21
+ /**
22
+ * An operation to authorize: its key, plus the optional level it declares.
23
+ * An operation with no `level` is reachable only through key patterns.
24
+ */
25
+ export interface Operation {
26
+ key: string;
27
+ level?: ToolLevel;
28
+ }
29
+ /**
30
+ * True iff `p` is exactly `level:read`, `level:write` or `level:admin` — the
31
+ * complete set of level patterns. Anything else (`level:*`, `level:owner`,
32
+ * `level:`) is not one.
33
+ */
34
+ export declare function isLevelPattern(p: string): boolean;
35
+ /**
36
+ * Returns true iff at least one grant pattern matches the operation.
37
+ * Allow-list semantics, default-deny. O(grants × segments), no backtracking.
38
+ *
39
+ * A bare string operation carries no level, so level patterns never match it.
40
+ * An `Operation` is matched by key patterns on `op.key` exactly as a string
41
+ * would be, and additionally by `level:X` iff `op.level === X`.
42
+ */
43
+ export declare function matchPermission(op: string | Operation, grants: readonly string[]): boolean;
44
+ /** Allow/deny permission set. Both lists use the same wildcard semantics. */
45
+ export interface Permissions {
46
+ allow: readonly string[];
47
+ deny: readonly string[];
48
+ }
49
+ /**
50
+ * Allow/deny decision: allowed iff some `allow` grant matches AND no `deny`
51
+ * grant matches. Deny overrides allow (even `allow: ['*']`). Default-deny is
52
+ * inherited from `matchPermission`: empty allow ⇒ false; empty deny ⇒ allow
53
+ * decides; blank key ⇒ false.
54
+ */
55
+ export declare function evaluate(op: string | Operation, perms: Permissions): boolean;
56
+ /**
57
+ * Structural validator for grant patterns (for UI/API input validation, e.g.
58
+ * custom-role CRUD). Valid iff non-empty, no whitespace, no empty segments, and
59
+ * every segment is either exactly `*` or a literal with no `*`. Does NOT enforce
60
+ * a character set — tool-id grants may use arbitrary segment characters.
61
+ */
62
+ export declare function isValidPattern(pattern: string): boolean;