@isi-ui7/bos7-shared 0.2.4 → 0.2.7

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.
@@ -2,6 +2,17 @@ export type Ui7FormDensity = "compact" | "comfortable";
2
2
 
3
3
  export type Ui7ThemeDensity = Ui7FormDensity | "normal";
4
4
 
5
+ /**
6
+ * Desktop form-panel width. Mobile + tablet (<1056px) always release the
7
+ * cap and stretch to 100% — the modifier only matters at ≥1056px.
8
+ *
9
+ * - "half" → 50% of page body
10
+ * - "two-thirds" → 66.6667% (default — readable column width on wide
11
+ * monitors, what the contract shipped with originally)
12
+ * - "full" → 100% (forms with many columns or wide tables inside)
13
+ */
14
+ export type Ui7FormWidth = "half" | "two-thirds" | "full";
15
+
5
16
  export interface Ui7FormVisualTokens {
6
17
  labelSpacing: "6px";
7
18
  labelFontSize: "12px";
@@ -58,6 +69,9 @@ export const UI7_FORM_VISUAL_CLASSNAMES = {
58
69
  scope: "ui7-form-contract",
59
70
  densityCompact: "ui7-form-contract--density-compact",
60
71
  densityComfortable: "ui7-form-contract--density-comfortable",
72
+ widthHalf: "ui7-form-contract--width-half",
73
+ widthTwoThirds: "ui7-form-contract--width-two-thirds",
74
+ widthFull: "ui7-form-contract--width-full",
61
75
  readonly: "ui7-form-contract--readonly",
62
76
  row: "ui7-form-row",
63
77
  section: "ui7-form-section",
@@ -76,15 +90,15 @@ export const UI7_FORM_VISUAL_CLASSNAMES = {
76
90
  export const UI7_FORM_DENSITY_TOKENS = {
77
91
  compact: {
78
92
  controlHeight: "2rem",
79
- rowGap: "0.75rem",
80
- sectionGap: "1rem",
81
- columnGap: "0.75rem",
93
+ rowGap: "1rem",
94
+ sectionGap: "1.25rem",
95
+ columnGap: "1rem",
82
96
  },
83
97
  comfortable: {
84
98
  controlHeight: "2.5rem",
85
- rowGap: "1rem",
99
+ rowGap: "1.25rem",
86
100
  sectionGap: "1.5rem",
87
- columnGap: "1rem",
101
+ columnGap: "1.25rem",
88
102
  },
89
103
  } as const;
90
104
 
@@ -107,9 +121,24 @@ export function getUi7FormDensityClassName(
107
121
  : UI7_FORM_VISUAL_CLASSNAMES.densityComfortable;
108
122
  }
109
123
 
124
+ export function getUi7FormWidthClassName(width?: Ui7FormWidth | null): string {
125
+ switch (width) {
126
+ case "half":
127
+ return UI7_FORM_VISUAL_CLASSNAMES.widthHalf;
128
+ case "full":
129
+ return UI7_FORM_VISUAL_CLASSNAMES.widthFull;
130
+ case "two-thirds":
131
+ default:
132
+ // Default is two-thirds — kept explicit (not relying on base CSS) so
133
+ // a future base-width change won't silently shift every form.
134
+ return UI7_FORM_VISUAL_CLASSNAMES.widthTwoThirds;
135
+ }
136
+ }
137
+
110
138
  export function getUi7FormContractClassName(
111
139
  options?: {
112
140
  density?: Ui7ThemeDensity | null;
141
+ width?: Ui7FormWidth | null;
113
142
  readonly?: boolean;
114
143
  className?: string | null;
115
144
  }
@@ -120,6 +149,10 @@ export function getUi7FormContractClassName(
120
149
  classes.push(getUi7FormDensityClassName(options.density));
121
150
  }
122
151
 
152
+ // Width: always emit a modifier so the choice is explicit at the DOM
153
+ // level. Default = two-thirds (matches what the contract shipped with).
154
+ classes.push(getUi7FormWidthClassName(options?.width));
155
+
123
156
  if (options?.readonly) {
124
157
  classes.push(UI7_FORM_VISUAL_CLASSNAMES.readonly);
125
158
  }
@@ -18,15 +18,22 @@ type CookieReqLike = Request & {
18
18
  cookies?: { get?: (name: string) => { value?: string } | undefined };
19
19
  };
20
20
 
21
- function decodeJWTSub(token: string): string {
21
+ type Auth7Claims = {
22
+ sub?: string;
23
+ preferred_username?: string;
24
+ org_id?: string;
25
+ branch_id?: string;
26
+ branch_code?: string;
27
+ };
28
+
29
+ function decodeJwtClaims(token: string): Auth7Claims {
22
30
  try {
23
31
  const parts = token.split('.');
24
- if (parts.length < 2) return '';
32
+ if (parts.length < 2) return {};
25
33
  const payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
26
- const claims = JSON.parse(Buffer.from(payload, 'base64').toString('utf-8')) as { sub?: string };
27
- return claims.sub ?? '';
34
+ return JSON.parse(Buffer.from(payload, 'base64').toString('utf-8')) as Auth7Claims;
28
35
  } catch {
29
- return '';
36
+ return {};
30
37
  }
31
38
  }
32
39
 
@@ -51,7 +58,23 @@ export type StartWorkflowInput = {
51
58
  flowId: string;
52
59
  payload: Record<string, unknown>;
53
60
  externalRef?: string;
54
- initiatorContext?: { user_id?: string; branch_id?: string };
61
+ /**
62
+ * Optional override for initiator identity. By default the helper decodes
63
+ * the full tenant identity (user_id, username, org_id, branch_id, branch_code)
64
+ * from the access token and uses that. Any field explicitly set here wins.
65
+ *
66
+ * Set ONLY when the caller has a verified reason to override (rare). For
67
+ * normal flow-start paths leave this undefined — letting the helper read
68
+ * the JWT keeps initiator_context aligned with the auth7 signature.
69
+ */
70
+ initiatorContext?: {
71
+ user_id?: string;
72
+ username?: string;
73
+ org_id?: string;
74
+ branch_id?: string;
75
+ branch_code?: string;
76
+ ip?: string;
77
+ };
55
78
  /** User access token dari auth7 — diforward ke workflow7 sebagai Bearer. */
56
79
  userAccessToken: string;
57
80
  };
@@ -64,11 +87,25 @@ export type StartWorkflowResult = {
64
87
  };
65
88
 
66
89
  export async function startWorkflow(input: StartWorkflowInput): Promise<StartWorkflowResult> {
67
- const userID = decodeJWTSub(input.userAccessToken);
68
- const initiatorContext: Record<string, unknown> = { ...(input.initiatorContext ?? {}) };
69
- if (!initiatorContext.user_id && userID) {
70
- initiatorContext.user_id = userID;
71
- }
90
+ // Decode the full tenant identity from the user JWT so workflow7 can pass
91
+ // through all five canonical actor fields to downstream service-task HTTP
92
+ // calls (lib7 ActorEnvelope / Wave 17 audit envelope). Decoding here keeps
93
+ // identity aligned with the auth7 signature — service-task verifiers can
94
+ // trust the envelope without re-checking subject claims.
95
+ const claims = decodeJwtClaims(input.userAccessToken);
96
+ const override = input.initiatorContext ?? {};
97
+ const initiatorContext: Record<string, unknown> = {};
98
+ type InitiatorKey = 'user_id' | 'username' | 'org_id' | 'branch_id' | 'branch_code';
99
+ const setIf = (k: InitiatorKey, fromClaim: string | undefined, fromOverride: unknown) => {
100
+ const v = fromOverride ?? fromClaim;
101
+ if (typeof v === 'string' && v !== '') initiatorContext[k] = v;
102
+ };
103
+ setIf('user_id', claims.sub, override.user_id);
104
+ setIf('username', claims.preferred_username, override.username);
105
+ setIf('org_id', claims.org_id, override.org_id);
106
+ setIf('branch_id', claims.branch_id, override.branch_id);
107
+ setIf('branch_code', claims.branch_code, override.branch_code);
108
+ if (typeof override.ip === 'string' && override.ip !== '') initiatorContext.ip = override.ip;
72
109
 
73
110
  const body: Record<string, unknown> = {
74
111
  flow_definition_code: input.flowId,