Skip to content

chore: add external policy example#36

Draft
marcos-cereijo-pexip wants to merge 1 commit into
masterfrom
feat/add-external-policy-example
Draft

chore: add external policy example#36
marcos-cereijo-pexip wants to merge 1 commit into
masterfrom
feat/add-external-policy-example

Conversation

@marcos-cereijo-pexip

Copy link
Copy Markdown
Collaborator

Closes #35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a self-contained “external policy” Node.js example that mirrors the repository’s local policy logic, addressing Issue #35.

Changes:

  • Introduces an Express-based external policy server with service configuration and participant properties endpoints.
  • Implements WebCrypto-based hashing helpers to support deterministic/dynamic PIN generation.
  • Adds standalone example packaging/docs and excludes the example directory from root ESLint scanning.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
external-policy-example/src/policy.mjs Implements policy decision logic for routing and dynamic PIN derivation.
external-policy-example/src/index.mjs Express server exposing the external policy endpoints.
external-policy-example/src/hash.mjs WebCrypto-based pex_hash/pex_tail equivalents used by the example.
external-policy-example/package.json Defines the example’s Node.js package and Express dependency.
external-policy-example/package-lock.json Locks dependency versions for the example package.
external-policy-example/README.md Documents endpoints, routing behavior, and setup instructions.
eslint.config.js Adds external-policy-example to ESLint ignore list.
Files not reviewed (1)
  • external-policy-example/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const response = await handleParticipantProperties(localAlias, remoteDisplayName, vendor)

console.log(
`[participant] local_alias=${localAlias} remote_display_name=${remoteDisplayName ?? ''} → call_tag=${response.result.call_tag ?? '(none)'}`

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log line dereferences response.result.call_tag, but handleParticipantProperties can return a response without a result field. This will throw at runtime for non-main-room aliases. Use optional chaining (response.result?.call_tag) or ensure the policy handler always includes result: {}.

Suggested change
`[participant] local_alias=${localAlias} remote_display_name=${remoteDisplayName ?? ''} → call_tag=${response.result.call_tag ?? '(none)'}`
`[participant] local_alias=${localAlias} remote_display_name=${remoteDisplayName ?? ''} → call_tag=${response.result?.call_tag ?? '(none)'}`

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +36
const { local_alias: localAlias, remote_display_name: remoteDisplayName, vendor } = req.query

const response = await handleServiceConfiguration(localAlias, remoteDisplayName, vendor)

console.log(
`[service-config] local_alias=${localAlias} → action=${response.action}, result=${JSON.stringify(response.result)}`
)

return res.json(response)
})

app.get('/policy/v1/participant/properties', async (req, res) => {
const {local_alias: localAlias, remote_display_name: remoteDisplayName, vendor } = req.query

const response = await handleParticipantProperties(localAlias, remoteDisplayName, vendor)

console.log(
`[participant] local_alias=${localAlias} remote_display_name=${remoteDisplayName ?? ''} → call_tag=${response.result.call_tag ?? '(none)'}`


)

return res.json(response)

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These route handlers are async and can throw/reject (e.g., hashing failures). In Express 4, rejected promises in route handlers are not reliably forwarded to error middleware, which can lead to unhandled rejections and no HTTP response. Wrap the handler body in try/catch and return a 5xx (or call next(err) with an error-handling middleware).

Suggested change
const { local_alias: localAlias, remote_display_name: remoteDisplayName, vendor } = req.query
const response = await handleServiceConfiguration(localAlias, remoteDisplayName, vendor)
console.log(
`[service-config] local_alias=${localAlias} → action=${response.action}, result=${JSON.stringify(response.result)}`
)
return res.json(response)
})
app.get('/policy/v1/participant/properties', async (req, res) => {
const {local_alias: localAlias, remote_display_name: remoteDisplayName, vendor } = req.query
const response = await handleParticipantProperties(localAlias, remoteDisplayName, vendor)
console.log(
`[participant] local_alias=${localAlias} remote_display_name=${remoteDisplayName ?? ''} → call_tag=${response.result.call_tag ?? '(none)'}`
)
return res.json(response)
try {
const { local_alias: localAlias, remote_display_name: remoteDisplayName, vendor } = req.query
const response = await handleServiceConfiguration(localAlias, remoteDisplayName, vendor)
console.log(
`[service-config] local_alias=${localAlias} → action=${response.action}, result=${JSON.stringify(response.result)}`
)
return res.json(response)
} catch (error) {
console.error('[service-config] request failed', error)
return res.status(500).json({ error: 'Internal Server Error' })
}
})
app.get('/policy/v1/participant/properties', async (req, res) => {
try {
const {local_alias: localAlias, remote_display_name: remoteDisplayName, vendor } = req.query
const response = await handleParticipantProperties(localAlias, remoteDisplayName, vendor)
console.log(
`[participant] local_alias=${localAlias} remote_display_name=${remoteDisplayName ?? ''} → call_tag=${response.result.call_tag ?? '(none)'}`
)
return res.json(response)
} catch (error) {
console.error('[participant] request failed', error)
return res.status(500).json({ error: 'Internal Server Error' })
}

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +78
const callTag = pexTail(
await pexHash(SECRET + mainRoomAlias + cleanVendor + cleanDisplayName),
20
)
pin = pexTail(await pexHash(callTag + 'interpreter'), 20)
guestPin = pexTail(await pexHash(callTag + 'listener'), 20)
}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pexHash can return undefined on error, but the result is passed into pexTail(...) (and later concatenated) which will throw when attempting .slice()/string concatenation. Handle the undefined case explicitly (e.g., return a policy reject/continue response, or throw a controlled error) before calling pexTail or building derived PINs.

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +9
const PORT = process.env.PORT ?? 3000

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PORT comes from process.env.PORT which is a string; passing a numeric string to app.listen() is interpreted as a pipe/path in Node’s net.Server.listen, not a TCP port. Convert it to a number (and validate/handle NaN) before calling listen.

Suggested change
const PORT = process.env.PORT ?? 3000
const rawPort = process.env.PORT
const PORT = rawPort == null ? 3000 : Number(rawPort)
if (!Number.isInteger(PORT)) {
throw new Error(`Invalid PORT: ${rawPort}`)
}

Copilot uses AI. Check for mistakes.

return {
status: 'success',
action: 'continue'

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function returns an object without result, while other branches return result: {...}. Since the server code assumes result exists, either always include result: {} here (for consistency) or ensure all consumers handle an absent result safely.

Suggested change
action: 'continue'
action: 'continue',
result: {}

Copilot uses AI. Check for mistakes.
* - Otherwise → continue and let the system handle it with default settings.
*
* @param {string} localAlias - The alias of the incoming call.
* @returns {{ action: string, result: object }}

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc for handleServiceConfiguration is out of sync with the implementation: the signature accepts (localAlias, remoteDisplayName, vendor) and the function returns status as well, but only localAlias and { action, result } are documented. Update the JSDoc params/return type to match the actual contract.

Suggested change
* @returns {{ action: string, result: object }}
* @param {string} remoteDisplayName - The display name of the participant.
* @param {string} vendor - The vendor string from the SIP User-Agent header.
* @returns {{ status: string, action: string, result?: object }}

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +25
// Changing this value invalidates all previously generated call tags.
const SECRET = 'my_secret_value'

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example hardcodes SECRET in source control. To avoid accidental reuse in real deployments, read the secret from an environment variable (and fail fast if missing) or make it clearly a placeholder value that must be replaced.

Suggested change
// Changing this value invalidates all previously generated call tags.
const SECRET = 'my_secret_value'
// Read it from the environment so it is not committed to source control.
// Changing this value invalidates all previously generated call tags.
const SECRET = DYNAMIC_PINS ? process.env.PEX_POLICY_SECRET?.trim() : undefined
if (DYNAMIC_PINS && !SECRET) {
throw new Error('PEX_POLICY_SECRET must be set when DYNAMIC_PINS is enabled')
}

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +19
// Convert to byte
const encoder = new TextEncoder()
const data = encoder.encode(input)

// Use crypto of navigator
const hashBuffer = await crypto.subtle.digest('SHA-256', data)

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pexHash relies on global crypto.subtle and TextEncoder. In Node.js these globals depend on the Node version; since package.json doesn’t declare an engines.node constraint, this example can fail at runtime on older Node versions. Either import WebCrypto explicitly from node:crypto or document/enforce the minimum Node version in package.json.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +29
| Alias format | Example | Behavior |
| ------------ | -------- | ---------------------------------------------------- |
| 2 digits | `01` | Main conference room with static PINs. |
| 6 digits | `010033` | Interpretation room (audio). PINs static or dynamic. |
| Other | — | Pass-through (if known) or reject. |

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The routing table says "Other → Pass-through (if known) or reject", but the implementation always returns action: 'continue' for unknown alias formats and never rejects. Align the documentation with the actual behavior, or change the policy to reject unknown aliases if that’s the intended behavior.

Suggested change
| Alias format | Example | Behavior |
| ------------ | -------- | ---------------------------------------------------- |
| 2 digits | `01` | Main conference room with static PINs. |
| 6 digits | `010033` | Interpretation room (audio). PINs static or dynamic. |
| Other || Pass-through (if known) or reject. |
| Alias format | Example | Behavior |
| ------------ | -------- | ------------------------------------------------------------- |
| 2 digits | `01` | Main conference room with static PINs. |
| 6 digits | `010033` | Interpretation room (audio). PINs static or dynamic. |
| Other || Pass-through (`action: 'continue'`); this example does not reject unknown aliases. |

Copilot uses AI. Check for mistakes.
@marcos-cereijo-pexip marcos-cereijo-pexip marked this pull request as draft April 27, 2026 11:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add external policy example

2 participants