@realsee/vr-cli 0.1.5 → 0.2.1

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,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto'
2
- import { chmod, lstat, open, rename, rm } from 'node:fs/promises'
2
+ import { chmod, lstat, mkdir, open, rename, rm } from 'node:fs/promises'
3
3
  import os from 'node:os'
4
4
  import path from 'node:path'
5
5
  import { spawn } from 'node:child_process'
@@ -8,6 +8,8 @@ export const SETUP_PAYLOAD_PREFIX = 'realsee-setup:v1:'
8
8
  export const DEFAULT_GATEWAY_ENDPOINT = 'https://app-gateway.realsee.cn'
9
9
 
10
10
  const RESOURCE_CODE_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
11
+ const GATEWAY_ENDPOINT_PATTERN =
12
+ /^https:\/\/[A-Za-z0-9.-]+(?::\d+)?(?:\/[A-Za-z0-9._~%/-]*)?$/u
11
13
  const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/
12
14
  const MAX_PAYLOAD_LENGTH = 8_192
13
15
  const MAX_CREDENTIAL_LENGTH = 1_024
@@ -16,7 +18,11 @@ function setupError(message) {
16
18
  return new Error(`配置包无效:${message}`)
17
19
  }
18
20
 
19
- function exactString(value, label) {
21
+ function credentialError(message) {
22
+ return new Error(`网关凭证无效:${message}`)
23
+ }
24
+
25
+ function exactString(value, label, makeError = setupError) {
20
26
  if (
21
27
  typeof value !== 'string' ||
22
28
  value.length === 0 ||
@@ -24,11 +30,44 @@ function exactString(value, label) {
24
30
  value !== value.trim() ||
25
31
  /[\0\r\n]/u.test(value)
26
32
  ) {
27
- throw setupError(`${label} 缺失或格式不正确。`)
33
+ throw makeError(`${label} 缺失或格式不正确。`)
28
34
  }
29
35
  return value
30
36
  }
31
37
 
38
+ // Normalizes credentials resolved outside the pasted payload (environment
39
+ // variables and the pairing protocol) into the exact shape the writer renders.
40
+ export function normalizeGatewayCredentials({
41
+ appKey,
42
+ appSecret,
43
+ resourceCode,
44
+ gatewayEndpoint,
45
+ }) {
46
+ const config = {
47
+ appKey: exactString(appKey, 'AK', credentialError),
48
+ appSecret: exactString(appSecret, 'SK', credentialError),
49
+ }
50
+ if (resourceCode !== undefined && resourceCode !== '') {
51
+ const value = exactString(resourceCode, 'resourceCode', credentialError)
52
+ if (!RESOURCE_CODE_PATTERN.test(value)) {
53
+ throw credentialError('resourceCode 格式不正确。')
54
+ }
55
+ config.resourceCode = value
56
+ }
57
+ if (gatewayEndpoint !== undefined && gatewayEndpoint !== '') {
58
+ const value = exactString(
59
+ gatewayEndpoint,
60
+ 'gatewayEndpoint',
61
+ credentialError,
62
+ )
63
+ if (!GATEWAY_ENDPOINT_PATTERN.test(value)) {
64
+ throw credentialError('gatewayEndpoint 必须是 HTTPS URL。')
65
+ }
66
+ config.gatewayEndpoint = value
67
+ }
68
+ return Object.freeze(config)
69
+ }
70
+
32
71
  export function decodeSetupPayload(input) {
33
72
  const value = typeof input === 'string' ? input.trim() : ''
34
73
  if (!value.startsWith(SETUP_PAYLOAD_PREFIX)) {
@@ -66,31 +105,49 @@ export function decodeSetupPayload(input) {
66
105
  throw setupError('内容结构不正确。')
67
106
  }
68
107
  const keys = Object.keys(parsed).sort()
69
- if (keys.join(',') !== 'appKey,appSecret,resourceCode') {
108
+ const keyList = keys.join(',')
109
+ if (
110
+ keyList !== 'appKey,appSecret' &&
111
+ keyList !== 'appKey,appSecret,resourceCode'
112
+ ) {
70
113
  throw setupError('字段集合不正确。')
71
114
  }
72
115
 
73
116
  const appKey = exactString(parsed.appKey, 'AK')
74
117
  const appSecret = exactString(parsed.appSecret, 'SK')
75
- const resourceCode = exactString(parsed.resourceCode, 'resourceCode')
76
- if (!RESOURCE_CODE_PATTERN.test(resourceCode)) {
77
- throw setupError('resourceCode 格式不正确。')
118
+ let resourceCode
119
+ if (parsed.resourceCode !== undefined && parsed.resourceCode !== '') {
120
+ resourceCode = exactString(parsed.resourceCode, 'resourceCode')
121
+ if (!RESOURCE_CODE_PATTERN.test(resourceCode)) {
122
+ throw setupError('resourceCode 格式不正确。')
123
+ }
78
124
  }
79
125
 
80
- return Object.freeze({ appKey, appSecret, resourceCode })
126
+ return Object.freeze({
127
+ appKey,
128
+ appSecret,
129
+ ...(resourceCode === undefined ? {} : { resourceCode }),
130
+ })
81
131
  }
82
132
 
83
133
  function quoteDotenvValue(value) {
84
134
  return JSON.stringify(value)
85
135
  }
86
136
 
87
- export function renderLocalEnv({ appKey, appSecret, resourceCode }) {
137
+ export function renderLocalEnv({
138
+ appKey,
139
+ appSecret,
140
+ resourceCode,
141
+ gatewayEndpoint,
142
+ }) {
88
143
  return [
89
144
  '# Generated by npm run setup. Keep this server-only file out of version control.',
90
- `REALSEE_GATEWAY_ENDPOINT=${DEFAULT_GATEWAY_ENDPOINT}`,
145
+ `REALSEE_GATEWAY_ENDPOINT=${gatewayEndpoint ?? DEFAULT_GATEWAY_ENDPOINT}`,
91
146
  `REALSEE_GATEWAY_APP_KEY=${quoteDotenvValue(appKey)}`,
92
147
  `REALSEE_GATEWAY_APP_SECRET=${quoteDotenvValue(appSecret)}`,
93
- `REALSEE_DEFAULT_RESOURCE_CODE=${quoteDotenvValue(resourceCode)}`,
148
+ ...(resourceCode
149
+ ? [`REALSEE_DEFAULT_RESOURCE_CODE=${quoteDotenvValue(resourceCode)}`]
150
+ : []),
94
151
  '',
95
152
  ].join('\n')
96
153
  }
@@ -144,27 +201,25 @@ async function assertSafeTarget(targetPath) {
144
201
  }
145
202
  }
146
203
 
147
- export async function writeLocalConfig(
148
- config,
149
- {
150
- root = process.cwd(),
151
- platform = process.platform,
152
- env = process.env,
153
- runCommand = run,
154
- } = {},
204
+ // Shared atomic write chain: restrictive umask, exclusive 0600 temp file,
205
+ // fsync, platform permission hardening, then rename into place. Used for
206
+ // `.env.development` and for the 0600 pairing continuation file.
207
+ export async function writePrivateFile(
208
+ targetPath,
209
+ content,
210
+ { platform = process.platform, env = process.env, runCommand = run } = {},
155
211
  ) {
156
- const targetPath = path.join(path.resolve(root), '.env.development')
157
- await assertSafeTarget(targetPath)
212
+ await mkdir(path.dirname(targetPath), { recursive: true, mode: 0o700 })
158
213
  const temporaryPath = path.join(
159
214
  path.dirname(targetPath),
160
- `.env.development.${process.pid}.${randomUUID()}.tmp`,
215
+ `.${path.basename(targetPath)}.${process.pid}.${randomUUID()}.tmp`,
161
216
  )
162
217
  const previousUmask = platform === 'win32' ? undefined : process.umask(0o077)
163
218
 
164
219
  try {
165
220
  const handle = await open(temporaryPath, 'wx', 0o600)
166
221
  try {
167
- await handle.writeFile(renderLocalEnv(config), 'utf8')
222
+ await handle.writeFile(content, 'utf8')
168
223
  await handle.sync()
169
224
  } finally {
170
225
  await handle.close()
@@ -177,3 +232,21 @@ export async function writeLocalConfig(
177
232
  await rm(temporaryPath, { force: true }).catch(() => undefined)
178
233
  }
179
234
  }
235
+
236
+ export async function writeLocalConfig(
237
+ config,
238
+ {
239
+ root = process.cwd(),
240
+ platform = process.platform,
241
+ env = process.env,
242
+ runCommand = run,
243
+ } = {},
244
+ ) {
245
+ const targetPath = path.join(path.resolve(root), '.env.development')
246
+ await assertSafeTarget(targetPath)
247
+ return writePrivateFile(targetPath, renderLocalEnv(config), {
248
+ platform,
249
+ env,
250
+ runCommand,
251
+ })
252
+ }
@@ -16,29 +16,59 @@ const CONFIG = Object.freeze({
16
16
  appSecret: '测试-secret_+/=',
17
17
  resourceCode: 'resource-code_1',
18
18
  })
19
+ const CONFIG_WITHOUT_RESOURCE = Object.freeze({
20
+ appKey: CONFIG.appKey,
21
+ appSecret: CONFIG.appSecret,
22
+ })
19
23
 
20
24
  const ENCODED =
21
25
  'eyJhcHBLZXkiOiJ0ZXN0LWFwcC1rZXkiLCJhcHBTZWNyZXQiOiLmtYvor5Utc2VjcmV0XysvPSIsInJlc291cmNlQ29kZSI6InJlc291cmNlLWNvZGVfMSJ9'
26
+ const encode = (value) =>
27
+ Buffer.from(JSON.stringify(value)).toString('base64url')
22
28
 
23
- test('decodes the versioned UTF-8 Base64URL setup payload', () => {
29
+ test('decodes the existing versioned payload with its optional resource fallback', () => {
24
30
  assert.deepEqual(
25
31
  decodeSetupPayload(`${SETUP_PAYLOAD_PREFIX}${ENCODED}`),
26
32
  CONFIG,
27
33
  )
28
34
  })
29
35
 
30
- test('rejects non-canonical, incomplete and extra-field payloads', () => {
36
+ test('accepts a v1 setup payload without a resource fallback', () => {
37
+ assert.deepEqual(
38
+ decodeSetupPayload(
39
+ `${SETUP_PAYLOAD_PREFIX}${encode(CONFIG_WITHOUT_RESOURCE)}`,
40
+ ),
41
+ CONFIG_WITHOUT_RESOURCE,
42
+ )
43
+ assert.deepEqual(
44
+ decodeSetupPayload(
45
+ `${SETUP_PAYLOAD_PREFIX}${encode({ ...CONFIG_WITHOUT_RESOURCE, resourceCode: '' })}`,
46
+ ),
47
+ CONFIG_WITHOUT_RESOURCE,
48
+ )
49
+ })
50
+
51
+ test('rejects non-canonical, malformed and extra-field payloads', () => {
31
52
  assert.throws(() => decodeSetupPayload('not-a-setup-payload'))
32
53
  assert.throws(() => decodeSetupPayload(`${SETUP_PAYLOAD_PREFIX}${ENCODED}=`))
33
- const withExtraField = Buffer.from(
34
- JSON.stringify({ ...CONFIG, unexpected: true }),
35
- ).toString('base64url')
36
54
  assert.throws(() =>
37
- decodeSetupPayload(`${SETUP_PAYLOAD_PREFIX}${withExtraField}`),
55
+ decodeSetupPayload(
56
+ `${SETUP_PAYLOAD_PREFIX}${encode({ appKey: CONFIG.appKey })}`,
57
+ ),
58
+ )
59
+ assert.throws(() =>
60
+ decodeSetupPayload(
61
+ `${SETUP_PAYLOAD_PREFIX}${encode({ ...CONFIG, resourceCode: 'invalid code' })}`,
62
+ ),
63
+ )
64
+ assert.throws(() =>
65
+ decodeSetupPayload(
66
+ `${SETUP_PAYLOAD_PREFIX}${encode({ ...CONFIG, unexpected: true })}`,
67
+ ),
38
68
  )
39
69
  })
40
70
 
41
- test('renders one server-only env file without printing the setup payload', () => {
71
+ test('renders the optional fallback only when it is configured', () => {
42
72
  const rendered = renderLocalEnv(CONFIG)
43
73
  assert.match(
44
74
  rendered,
@@ -48,14 +78,24 @@ test('renders one server-only env file without printing the setup payload', () =
48
78
  assert.match(rendered, /REALSEE_GATEWAY_APP_SECRET="测试-secret_\+\/="/u)
49
79
  assert.match(rendered, /REALSEE_DEFAULT_RESOURCE_CODE="resource-code_1"/u)
50
80
  assert.doesNotMatch(rendered, /realsee-setup:v1:/u)
81
+
82
+ const withoutResource = renderLocalEnv(CONFIG_WITHOUT_RESOURCE)
83
+ assert.match(withoutResource, /REALSEE_GATEWAY_APP_KEY="test-app-key"/u)
84
+ assert.doesNotMatch(withoutResource, /REALSEE_DEFAULT_RESOURCE_CODE/u)
51
85
  })
52
86
 
53
87
  test('writes atomically with restrictive POSIX permissions', async () => {
54
88
  if (process.platform === 'win32') return
55
89
  const root = await mkdtemp(path.join(os.tmpdir(), 'realsee-setup-'))
56
90
  try {
57
- const target = await writeLocalConfig(CONFIG, { root, platform: 'linux' })
58
- assert.equal(await readFile(target, 'utf8'), renderLocalEnv(CONFIG))
91
+ const target = await writeLocalConfig(CONFIG_WITHOUT_RESOURCE, {
92
+ root,
93
+ platform: 'linux',
94
+ })
95
+ assert.equal(
96
+ await readFile(target, 'utf8'),
97
+ renderLocalEnv(CONFIG_WITHOUT_RESOURCE),
98
+ )
59
99
  assert.equal((await stat(target)).mode & 0o777, 0o600)
60
100
  } finally {
61
101
  await rm(root, { recursive: true, force: true })
@@ -1,7 +1,33 @@
1
1
  import { pathToFileURL } from 'node:url'
2
+ import { parseArgs } from 'node:util'
2
3
  import { Writable } from 'node:stream'
3
4
  import { createInterface } from 'node:readline/promises'
4
- import { decodeSetupPayload, writeLocalConfig } from './setup-config.mjs'
5
+ import {
6
+ decodeSetupPayload,
7
+ normalizeGatewayCredentials,
8
+ writeLocalConfig,
9
+ } from './setup-config.mjs'
10
+ import {
11
+ PAIRING_EXIT_CODES,
12
+ maskAppKey,
13
+ reportPairingFailure,
14
+ runPairingCommand,
15
+ runPairingCompleteCommand,
16
+ } from './pairing.mjs'
17
+
18
+ const HELP = `用法:npm run setup [-- <选项>]
19
+
20
+ 默认走配对:发起请求,打印验证链接与短码,阻塞轮询到终态并写入 .env.development。
21
+ --pair 显式选择配对路径(默认行为)。
22
+ --no-wait 发起后立即退出(退出码 2),稍后用 --complete 续做。
23
+ --complete 读取 .realsee/pairing-continuation.json,轮询到终态并写入。
24
+ --paste 回退路径:把 Portal 复制的单行配置包粘贴到隐藏输入。
25
+ --json 只输出 JSON 信封,不打印人类可读提示。
26
+ -h, --help 显示本帮助。
27
+
28
+ 环境变量 REALSEE_GATEWAY_APP_KEY / REALSEE_GATEWAY_APP_SECRET 优先级最高,
29
+ 设置后跳过配对,直接写入 .env.development。
30
+ `
5
31
 
6
32
  class HiddenOutput extends Writable {
7
33
  _write(_chunk, _encoding, callback) {
@@ -33,17 +59,145 @@ async function readHiddenPayload() {
33
59
  }
34
60
  }
35
61
 
36
- export async function main() {
62
+ async function runPasteFlow() {
37
63
  const payload = decodeSetupPayload(await readHiddenPayload())
38
64
  await writeLocalConfig(payload)
39
65
  console.log('配置完成。请回到 AI 助手并回复“已完成”。')
40
66
  }
41
67
 
68
+ function parseSetupArguments(argv) {
69
+ try {
70
+ return parseArgs({
71
+ args: argv,
72
+ allowPositionals: false,
73
+ strict: true,
74
+ options: {
75
+ pair: { type: 'boolean', default: false },
76
+ paste: { type: 'boolean', default: false },
77
+ complete: { type: 'boolean', default: false },
78
+ 'no-wait': { type: 'boolean', default: false },
79
+ json: { type: 'boolean', default: false },
80
+ help: { type: 'boolean', short: 'h', default: false },
81
+ },
82
+ }).values
83
+ } catch (error) {
84
+ throw new Error(
85
+ `参数错误:${error instanceof Error ? error.message : String(error)}`,
86
+ { cause: error },
87
+ )
88
+ }
89
+ }
90
+
91
+ function resolveMode(values) {
92
+ if (values.paste) {
93
+ const conflicting = ['pair', 'complete', 'no-wait', 'json'].find(
94
+ (flag) => values[flag] === true,
95
+ )
96
+ if (conflicting) {
97
+ throw new Error(`--paste 不能与 --${conflicting} 同时使用。`)
98
+ }
99
+ return 'paste'
100
+ }
101
+ if (values.complete) {
102
+ if (values['no-wait']) {
103
+ throw new Error('--complete 不能与 --no-wait 同时使用。')
104
+ }
105
+ return 'complete'
106
+ }
107
+ return 'pair'
108
+ }
109
+
110
+ function environmentCredentials(env = process.env) {
111
+ const appKey = env.REALSEE_GATEWAY_APP_KEY?.trim()
112
+ const appSecret = env.REALSEE_GATEWAY_APP_SECRET?.trim()
113
+ if (!appKey && !appSecret) return null
114
+ if (!appKey || !appSecret) {
115
+ throw new Error(
116
+ 'REALSEE_GATEWAY_APP_KEY 与 REALSEE_GATEWAY_APP_SECRET 必须同时提供。',
117
+ )
118
+ }
119
+ const resourceCode = env.REALSEE_DEFAULT_RESOURCE_CODE?.trim()
120
+ const gatewayEndpoint = env.REALSEE_GATEWAY_ENDPOINT?.trim()
121
+ return normalizeGatewayCredentials({
122
+ appKey,
123
+ appSecret,
124
+ ...(resourceCode ? { resourceCode } : {}),
125
+ ...(gatewayEndpoint ? { gatewayEndpoint } : {}),
126
+ })
127
+ }
128
+
129
+ async function configureFromEnvironment(config, { json }) {
130
+ await writeLocalConfig(config)
131
+ if (json !== true) {
132
+ process.stderr.write(
133
+ '检测到 REALSEE_GATEWAY_APP_KEY / REALSEE_GATEWAY_APP_SECRET 环境变量(优先级最高),已跳过配对并写入 .env.development。\n',
134
+ )
135
+ }
136
+ process.stdout.write(
137
+ `${JSON.stringify({
138
+ ok: true,
139
+ data: {
140
+ status: 'configured',
141
+ source: 'environment',
142
+ app_key: maskAppKey(config.appKey),
143
+ path: '.env.development',
144
+ },
145
+ })}\n`,
146
+ )
147
+ return PAIRING_EXIT_CODES.OK
148
+ }
149
+
150
+ export async function main(argv = process.argv.slice(2)) {
151
+ let values
152
+ try {
153
+ values = parseSetupArguments(argv)
154
+ } catch (error) {
155
+ return reportPairingFailure(error, { json: false })
156
+ }
157
+
158
+ if (values.help) {
159
+ process.stdout.write(HELP)
160
+ return PAIRING_EXIT_CODES.OK
161
+ }
162
+
163
+ let mode
164
+ try {
165
+ mode = resolveMode(values)
166
+ } catch (error) {
167
+ return reportPairingFailure(error, { json: values.json === true })
168
+ }
169
+
170
+ // The paste fallback keeps its original interactive behaviour.
171
+ if (mode === 'paste') {
172
+ await runPasteFlow()
173
+ return PAIRING_EXIT_CODES.OK
174
+ }
175
+
176
+ const json = values.json === true
177
+ try {
178
+ const config = environmentCredentials()
179
+ if (config) return await configureFromEnvironment(config, { json })
180
+ } catch (error) {
181
+ return reportPairingFailure(error, { json })
182
+ }
183
+
184
+ if (mode === 'complete') {
185
+ return await runPairingCompleteCommand({ json })
186
+ }
187
+ return await runPairingCommand({ noWait: values['no-wait'] === true, json })
188
+ }
189
+
42
190
  const entrypoint = process.argv[1]
43
191
  if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) {
44
- main().catch((error) => {
45
- const message = error instanceof Error ? error.message : '未知错误。'
46
- console.error(`本地配置失败:${message}`)
47
- process.exitCode = 1
48
- })
192
+ main()
193
+ .then((exitCode) => {
194
+ if (typeof exitCode === 'number' && exitCode !== 0) {
195
+ process.exitCode = exitCode
196
+ }
197
+ })
198
+ .catch((error) => {
199
+ const message = error instanceof Error ? error.message : '未知错误。'
200
+ console.error(`本地配置失败:${message}`)
201
+ process.exitCode = 1
202
+ })
49
203
  }
@@ -9,7 +9,7 @@ const PROJECT_ROOT = fileURLToPath(new URL('..', import.meta.url))
9
9
 
10
10
  export interface CreateAppOptions {
11
11
  readonly workService: WorkService
12
- readonly defaultResourceCode: string
12
+ readonly defaultResourceCode: string | undefined
13
13
  readonly logger?: boolean
14
14
  readonly serveFrontend?: boolean
15
15
  readonly frontendRoot?: string
@@ -21,11 +21,12 @@ function requiredCredential(env: ServerEnv, key: string): string {
21
21
  return value
22
22
  }
23
23
 
24
- function requiredResourceCode(env: ServerEnv): string {
24
+ function optionalResourceCode(env: ServerEnv): string | undefined {
25
25
  const value = env.REALSEE_DEFAULT_RESOURCE_CODE?.trim()
26
- if (!nonEmptyString(value) || !RESOURCE_CODE_PATTERN.test(value)) {
26
+ if (!nonEmptyString(value)) return undefined
27
+ if (!RESOURCE_CODE_PATTERN.test(value)) {
27
28
  throw new Error(
28
- 'REALSEE_DEFAULT_RESOURCE_CODE 未配置或格式无效。请运行 npm run setup 完成本地配置。',
29
+ 'REALSEE_DEFAULT_RESOURCE_CODE 格式无效,必须匹配 ^[A-Za-z0-9_-]{1,128}$。',
29
30
  )
30
31
  }
31
32
  return value
@@ -59,6 +60,6 @@ export function loadServerConfig({
59
60
  gatewayEndpoint: gatewayEndpoint.href.replace(/\/$/, ''),
60
61
  appKey: requiredCredential(env, 'REALSEE_GATEWAY_APP_KEY'),
61
62
  appSecret: requiredCredential(env, 'REALSEE_GATEWAY_APP_SECRET'),
62
- defaultResourceCode: requiredResourceCode(env),
63
+ defaultResourceCode: optionalResourceCode(env),
63
64
  })
64
65
  }
@@ -11,7 +11,7 @@ export interface ServerConfig {
11
11
  readonly gatewayEndpoint: string
12
12
  readonly appKey: string
13
13
  readonly appSecret: string
14
- readonly defaultResourceCode: string
14
+ readonly defaultResourceCode: string | undefined
15
15
  }
16
16
 
17
17
  export interface RequestOptions {
@@ -5,7 +5,7 @@ const RESOURCE_CODE_PATTERN = '^[A-Za-z0-9_-]{1,128}$'
5
5
 
6
6
  export interface WorkRoutesOptions {
7
7
  readonly workService: WorkService
8
- readonly defaultResourceCode: string
8
+ readonly defaultResourceCode: string | undefined
9
9
  }
10
10
 
11
11
  interface WorkRouteParams {
@@ -59,8 +59,19 @@ export function workRoutes(
59
59
  }
60
60
  }
61
61
 
62
- app.get('/api/work', async (request, reply) =>
63
- getWork(request, reply, defaultResourceCode),
62
+ app.get(
63
+ '/api/work',
64
+ async (request, reply): Promise<WorkEnvelope | FastifyReply> => {
65
+ if (!defaultResourceCode) {
66
+ return reply.code(400).send({
67
+ error: 'RESOURCE_CODE_REQUIRED',
68
+ message:
69
+ 'Provide resourceCode at /api/work/{resourceCode} or configure the optional REALSEE_DEFAULT_RESOURCE_CODE fallback.',
70
+ requestId: request.id,
71
+ })
72
+ }
73
+ return getWork(request, reply, defaultResourceCode)
74
+ },
64
75
  )
65
76
 
66
77
  app.get(