@sasjs/api 0.10.0 → 0.11.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.
@@ -71,6 +71,12 @@ export const dispatch = async (services, routeId, request = {}) => {
71
71
  const body = request.body || {};
72
72
  const params = request.params || {};
73
73
  /** The record service, or the 501 a host without one answers. */
74
+ /** The host's sessions, or the 501 that names the missing capability. */
75
+ const sessionRequired = () => {
76
+ if (!services.session)
77
+ throw new ApiError(501, `This host has no session service, so it cannot answer ${routeId}.`);
78
+ return services.session;
79
+ };
74
80
  const required = () => {
75
81
  if (!records)
76
82
  throw new ApiError(501, `This host has no record service, so it cannot answer ${routeId}.`);
@@ -168,6 +174,31 @@ export const dispatch = async (services, routeId, request = {}) => {
168
174
  }
169
175
  case 'GET /SASjsApi/drive/filetree':
170
176
  return { status: 200, body: await drive.tree(query._folderPath) };
177
+ // ------------------------------------------------------------- sessions
178
+ case 'GET /SASjsApi/session/{sessionId}': {
179
+ return {
180
+ status: 200,
181
+ body: await sessionRequired().summary(await caller(), String(params.sessionId ?? ''))
182
+ };
183
+ }
184
+ case 'GET /SASjsApi/session/{sessionId}/log': {
185
+ return {
186
+ status: 200,
187
+ contents: await sessionRequired().log(await caller(), String(params.sessionId ?? ''))
188
+ };
189
+ }
190
+ case 'GET /SASjsApi/session/{sessionId}/output': {
191
+ return {
192
+ status: 200,
193
+ contents: await sessionRequired().output(await caller(), String(params.sessionId ?? ''))
194
+ };
195
+ }
196
+ case 'GET /SASjsApi/session/{sessionId}/state': {
197
+ return {
198
+ status: 200,
199
+ body: await sessionRequired().state(await caller(), String(params.sessionId ?? ''))
200
+ };
201
+ }
171
202
  // ------------------------------------------------------------- records
172
203
  //
173
204
  // The administration routes: users, groups, permission rules, clients and the
@@ -353,6 +384,10 @@ export const dispatch = async (services, routeId, request = {}) => {
353
384
  };
354
385
  /** Whether a route is one `dispatch` can answer today. */
355
386
  export const HANDLED_ROUTES = [
387
+ 'GET /SASjsApi/session/{sessionId}',
388
+ 'GET /SASjsApi/session/{sessionId}/log',
389
+ 'GET /SASjsApi/session/{sessionId}/output',
390
+ 'GET /SASjsApi/session/{sessionId}/state',
356
391
  'POST /SASjsApi/code/lint',
357
392
  'POST /SASjsApi/code/format',
358
393
  'GET /SASjsApi/info/authorizedRoutes',
@@ -38,6 +38,7 @@ export type { IdentityFixture, IdentityFixtureClient, IdentityFixtureCode, Ident
38
38
  export type { IdentityClient, IdentityClientRecord, IdentityGroupRecord, IdentityPermissionRecord, IdentityStore, IdentityUser, IdentityUserRecord, NewGroup, NewPermission, NewUser, Subject, TokenPair, UserChange } from './identity/types.js';
39
39
  export { DRIVE_CORPUS } from './corpus/drive.js';
40
40
  export { IDENTITY_CORPUS } from './corpus/identity.js';
41
+ export { SESSION_CORPUS } from './corpus/session.js';
41
42
  export { RECORD_CORPUS, ADMIN_SIGNED_IN, PROVIDER_MANAGED } from './corpus/records.js';
42
43
  export type { CorpusCase } from './corpus/drive.js';
43
44
  export { runCase, servicesFor } from './corpus/runner.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EAClB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAG7D,cAAc,2BAA2B,CAAA;AACzC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACb,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,OAAO,EACP,OAAO,EACP,SAAS,EACT,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EACL,aAAa,EACb,eAAe,EACf,gBAAgB,EACjB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EAClB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAG7D,cAAc,2BAA2B,CAAA;AACzC,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAChF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,aAAa,EACb,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,EACb,mBAAmB,EACnB,YAAY,EACb,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,OAAO,EACP,OAAO,EACP,SAAS,EACT,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EACL,aAAa,EACb,eAAe,EACf,gBAAgB,EACjB,MAAM,qBAAqB,CAAA;AAC5B,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
@@ -30,5 +30,6 @@ export { MemoryIdentityStore } from './identity/memory-store.js';
30
30
  export { DEFAULT_IDENTITY, IDENTITY_FORMAT, parseIdentity } from './identity/fixture.js';
31
31
  export { DRIVE_CORPUS } from './corpus/drive.js';
32
32
  export { IDENTITY_CORPUS } from './corpus/identity.js';
33
+ export { SESSION_CORPUS } from './corpus/session.js';
33
34
  export { RECORD_CORPUS, ADMIN_SIGNED_IN, PROVIDER_MANAGED } from './corpus/records.js';
34
35
  export { runCase, servicesFor } from './corpus/runner.js';
@@ -1 +1 @@
1
- {"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../../src/routes/generated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,eAAO,MAAM,gBAAgB,EAAE,cAAc,EAs3D5C,CAAA"}
1
+ {"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../../src/routes/generated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,eAAO,MAAM,gBAAgB,EAAE,cAAc,EAw4D5C,CAAA"}
@@ -344,6 +344,14 @@ export const GENERATED_ROUTES = [
344
344
  {
345
345
  status: 200,
346
346
  description: 'Ok'
347
+ },
348
+ {
349
+ status: 400,
350
+ description: '"code" is required / "code" must be a string / "code" is not allowed to be empty'
351
+ },
352
+ {
353
+ status: 401,
354
+ description: 'Unauthorized'
347
355
  }
348
356
  ]
349
357
  },
@@ -372,6 +380,14 @@ export const GENERATED_ROUTES = [
372
380
  {
373
381
  status: 200,
374
382
  description: 'Ok'
383
+ },
384
+ {
385
+ status: 400,
386
+ description: '"code" is required / "code" must be a string / "code" is not allowed to be empty'
387
+ },
388
+ {
389
+ status: 401,
390
+ description: 'Unauthorized'
375
391
  }
376
392
  ]
377
393
  },
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AA2CD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAyNhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AAwCD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAgNhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
@@ -24,7 +24,6 @@ export const TAG_CAPABILITIES = {
24
24
  */
25
25
  const DIRECTORY = 'Synchronising with an external auth provider needs a directory port, which this package does not have: a host decides which directory it holds accounts in, and answers this itself.';
26
26
  const RUNTIME = 'Starting a program needs a runtime port, which this package does not have: a host decides which languages it holds, and answers this itself until then.';
27
- const SESSION_RECORD = "A triggered session's record, log, output and state belong to the runtime that ran the program, and this package has no runtime port.";
28
27
  const HOST_CONFIG = "The body is the host's own configuration - ports, run times and providers - which a mock states for itself and this package cannot know.";
29
28
  const HOST_OWNED = "The host owns this report: it describes the host's own process and files, which a browser build has no equivalent of.";
30
29
  const WEB_BUNDLE = 'The host serves its own web bundle; there is no API decision here for this package to answer.';
@@ -202,22 +201,13 @@ export const OVERLAY = {
202
201
  implemented: true,
203
202
  responses: { 200: user }
204
203
  },
205
- 'GET /SASjsApi/session/{sessionId}': {
206
- implemented: false,
207
- reason: SESSION_RECORD
208
- },
209
- 'GET /SASjsApi/session/{sessionId}/log': {
210
- implemented: false,
211
- reason: SESSION_RECORD
212
- },
213
- 'GET /SASjsApi/session/{sessionId}/output': {
214
- implemented: false,
215
- reason: SESSION_RECORD
216
- },
217
- 'GET /SASjsApi/session/{sessionId}/state': {
218
- implemented: false,
219
- reason: SESSION_RECORD
220
- },
204
+ // The port takes the owner as a uid and falls back to a username, which is
205
+ // what sasjs/server records today - a host hands its sessions through as they
206
+ // are, and the comparison tries the identifier first.
207
+ 'GET /SASjsApi/session/{sessionId}': { implemented: true },
208
+ 'GET /SASjsApi/session/{sessionId}/log': { implemented: true },
209
+ 'GET /SASjsApi/session/{sessionId}/output': { implemented: true },
210
+ 'GET /SASjsApi/session/{sessionId}/state': { implemented: true },
221
211
  // STP - a stored program, on the runtime port
222
212
  'GET /SASjsApi/stp/execute': { implemented: false, reason: RUNTIME },
223
213
  'POST /SASjsApi/stp/execute': { implemented: false, reason: RUNTIME },
@@ -0,0 +1,19 @@
1
+ import type { HostSession, SessionHost, SessionStateResponse } from './service.js';
2
+ /**
3
+ * The sessions the corpus and the package's own tests read.
4
+ *
5
+ * One session per account, so the interesting case is expressible: an
6
+ * administrator may read a session that is not theirs, and the other way round
7
+ * answers the 404 that hides it. `sess-silent` exists with nothing written yet,
8
+ * because an empty log is a 200 and not a 404.
9
+ */
10
+ export declare const DEFAULT_SESSIONS: Record<string, HostSession>;
11
+ export declare class MemorySessionHost implements SessionHost {
12
+ private readonly sessions;
13
+ constructor(sessions?: Record<string, HostSession>);
14
+ session(id: string): Promise<HostSession>;
15
+ state(id: string): Promise<SessionStateResponse | undefined>;
16
+ log(id: string): Promise<string | undefined>;
17
+ output(id: string): Promise<string | undefined>;
18
+ }
19
+ //# sourceMappingURL=memory-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-host.d.ts","sourceRoot":"","sources":["../../../src/session/memory-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,oBAAoB,EACrB,MAAM,cAAc,CAAA;AAErB;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CA8BxD,CAAA;AAmBD,qBAAa,iBAAkB,YAAW,WAAW;IACvC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,8BAAmB;IAElD,OAAO,CAAC,EAAE,EAAE,MAAM;IAIlB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAI5D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAI5C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;CAGtD"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The sessions the corpus and the package's own tests read.
3
+ *
4
+ * One session per account, so the interesting case is expressible: an
5
+ * administrator may read a session that is not theirs, and the other way round
6
+ * answers the 404 that hides it. `sess-silent` exists with nothing written yet,
7
+ * because an empty log is a 200 and not a 404.
8
+ */
9
+ export const DEFAULT_SESSIONS = {
10
+ 'sess-admin': {
11
+ owner: 'user-admin',
12
+ summary: {
13
+ sessionId: 'sess-admin',
14
+ state: 'completed',
15
+ pid: 4242,
16
+ creationTimeStamp: '2026-11-07T09:00:00.000Z',
17
+ deathTimeStamp: '2026-11-07T09:00:04.000Z',
18
+ expiresAfterMins: 60
19
+ }
20
+ },
21
+ 'sess-restricted': {
22
+ owner: 'user-restricted',
23
+ summary: {
24
+ sessionId: 'sess-restricted',
25
+ state: 'running',
26
+ pid: 4243,
27
+ creationTimeStamp: '2026-11-07T09:05:00.000Z'
28
+ }
29
+ },
30
+ 'sess-silent': {
31
+ owner: 'user-admin',
32
+ summary: {
33
+ sessionId: 'sess-silent',
34
+ state: 'running',
35
+ pid: 4244,
36
+ creationTimeStamp: '2026-11-07T09:10:00.000Z'
37
+ }
38
+ }
39
+ };
40
+ const STATES = {
41
+ 'sess-admin': { state: 'completed', pid: 4242, lineCount: 12 },
42
+ 'sess-restricted': { state: 'running', pid: 4243, lineCount: 3 }
43
+ };
44
+ const LOGS = {
45
+ 'sess-admin': 'NOTE: session started\nNOTE: session completed\n',
46
+ 'sess-restricted': 'NOTE: session started\n',
47
+ 'sess-silent': ''
48
+ };
49
+ const OUTPUTS = {
50
+ 'sess-admin': '<html><body>42</body></html>',
51
+ 'sess-restricted': '',
52
+ 'sess-silent': ''
53
+ };
54
+ export class MemorySessionHost {
55
+ constructor(sessions = DEFAULT_SESSIONS) {
56
+ this.sessions = sessions;
57
+ }
58
+ async session(id) {
59
+ return this.sessions[id];
60
+ }
61
+ async state(id) {
62
+ return STATES[id];
63
+ }
64
+ async log(id) {
65
+ return LOGS[id];
66
+ }
67
+ async output(id) {
68
+ return OUTPUTS[id];
69
+ }
70
+ }
@@ -0,0 +1,62 @@
1
+ import type { Subject } from '../identity/types.js';
2
+ import type { IdentityStore } from '../identity/types.js';
3
+ /** A triggered session as the API reports one. */
4
+ export interface SessionSummary {
5
+ sessionId: string;
6
+ state: string;
7
+ pid?: number;
8
+ creationTimeStamp?: string;
9
+ deathTimeStamp?: string;
10
+ failureReason?: string;
11
+ expiresAfterMins?: number;
12
+ }
13
+ /** What `/state` answers: whatever the host's run time reports for a session. */
14
+ export type SessionStateResponse = Record<string, unknown>;
15
+ /**
16
+ * A session as the host holds it.
17
+ *
18
+ * `owner` should be a **uid**: a username is mutable client data - an account
19
+ * can be renamed, and two accounts can swap names - so a name comparison can
20
+ * hand a session to whoever holds the name later. A username is accepted too,
21
+ * because sasjs/server's own session records carry the owner's username, and a
22
+ * host that hands those straight through the port must not 404 the owner of
23
+ * every session. The uid is tried first; the name is the fallback.
24
+ */
25
+ export interface HostSession {
26
+ owner: string;
27
+ summary: SessionSummary;
28
+ }
29
+ /**
30
+ * The host's sessions: a run time's registry, and the files that session wrote.
31
+ *
32
+ * Every capability answers `undefined` for a session the host does not hold,
33
+ * which the service turns into a 404. `log` and `output` answer an empty string
34
+ * for one that exists but has written nothing yet, because that is not an error.
35
+ */
36
+ export interface SessionHost {
37
+ session(id: string): Promise<HostSession | undefined>;
38
+ state(id: string): Promise<SessionStateResponse | undefined>;
39
+ log(id: string): Promise<string | undefined>;
40
+ output(id: string): Promise<string | undefined>;
41
+ }
42
+ export interface SessionService {
43
+ summary(caller: Subject | undefined, id: string): Promise<SessionSummary>;
44
+ state(caller: Subject | undefined, id: string): Promise<SessionStateResponse>;
45
+ log(caller: Subject | undefined, id: string): Promise<string>;
46
+ output(caller: Subject | undefined, id: string): Promise<string>;
47
+ }
48
+ /**
49
+ * The four session routes, over the host's own session store.
50
+ *
51
+ * The caller passes a `Subject` and nothing else: which account that is, whether
52
+ * it is an administrator, and whether the session is theirs are all resolved
53
+ * here, against the same store the records service reads. A host never states a
54
+ * role.
55
+ *
56
+ * `404` covers both "no such session" and "not yours", on purpose: a session id
57
+ * is handed to the caller that triggered the program and is built from a
58
+ * timestamp plus a small random number, so it is guessable within a window, and
59
+ * confirming the id exists would say it was right.
60
+ */
61
+ export declare const createSessionService: (store: IdentityStore, host: SessionHost) => SessionService;
62
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/session/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,sBAAsB,CAAA;AAE7E,kDAAkD;AAClD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,iFAAiF;AACjF,MAAM,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAE1D;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,cAAc,CAAA;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;IACrD,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAA;IAC5D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAC5C,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CAChD;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IACzE,KAAK,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAA;IAC7E,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAC7D,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;CACjE;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAC/B,OAAO,aAAa,EACpB,MAAM,WAAW,KAChB,cAsDF,CAAA"}
@@ -0,0 +1,61 @@
1
+ import { ApiError } from '../types.js';
2
+ /**
3
+ * The four session routes, over the host's own session store.
4
+ *
5
+ * The caller passes a `Subject` and nothing else: which account that is, whether
6
+ * it is an administrator, and whether the session is theirs are all resolved
7
+ * here, against the same store the records service reads. A host never states a
8
+ * role.
9
+ *
10
+ * `404` covers both "no such session" and "not yours", on purpose: a session id
11
+ * is handed to the caller that triggered the program and is built from a
12
+ * timestamp plus a small random number, so it is guessable within a window, and
13
+ * confirming the id exists would say it was right.
14
+ */
15
+ export const createSessionService = (store, host) => {
16
+ /** The server's wording, so adopting it here does not become a later change. */
17
+ const notFound = (id) => `Session with ID '${id}' was not found.`;
18
+ /** The caller, and the session if they may see it - or the 404 that hides it. */
19
+ const visible = async (caller, id) => {
20
+ if (!caller)
21
+ throw new ApiError(401, 'Unauthorized');
22
+ const reader = await store.userById(caller.userId);
23
+ if (!reader || !reader.isActive)
24
+ throw new ApiError(401, 'Unauthorized');
25
+ const session = await host.session(id);
26
+ if (!session)
27
+ throw new ApiError(404, notFound(id));
28
+ // the uid is the key to compare; a name is the fallback a host may need
29
+ const owner = session.owner;
30
+ if (owner !== reader.uid && owner !== reader.username && !reader.isAdmin)
31
+ throw new ApiError(404, notFound(id));
32
+ return session;
33
+ };
34
+ return {
35
+ async summary(caller, id) {
36
+ const session = await visible(caller, id);
37
+ return session.summary;
38
+ },
39
+ async state(caller, id) {
40
+ await visible(caller, id);
41
+ const state = await host.state(id);
42
+ if (!state)
43
+ throw new ApiError(404, notFound(id));
44
+ return state;
45
+ },
46
+ async log(caller, id) {
47
+ await visible(caller, id);
48
+ const log = await host.log(id);
49
+ if (log === undefined)
50
+ throw new ApiError(404, notFound(id));
51
+ return log;
52
+ },
53
+ async output(caller, id) {
54
+ await visible(caller, id);
55
+ const output = await host.output(id);
56
+ if (output === undefined)
57
+ throw new ApiError(404, notFound(id));
58
+ return output;
59
+ }
60
+ };
61
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sasjs/api",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "The SASjs Server API, described once - a generated route table, the services behind it, and the conformance corpus both hosts run",
5
5
  "license": "MIT",
6
6
  "author": "SASjs",